update posts for torchlight

This commit is contained in:
John Bowdre 2023-11-05 22:22:22 -06:00
parent 9ed755ff86
commit 47301abe76
32 changed files with 770 additions and 693 deletions

View file

@ -52,7 +52,8 @@ Now to reference these specs from a cloud template...
### Cloud template
I want to make sure that users requesting a deployment are able to pick whether or not a system should be joined to the domain, so I'm going to add that as an input option on the template:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
inputs:
[...]
adJoin:
@ -66,7 +67,8 @@ This new `adJoin` input is a boolean so it will appear on the request form as a
In the `resources` section of the template, I'll set a new property called `ignoreActiveDirectory` to be the inverse of the `adJoin` input; that will tell the AD integration not to do anything if the box to join the VM to the domain is unchecked. I'll also use `activeDirectory: relativeDN` to insert the appropriate site code into the DN where the computer object will be created. And, finally, I'll reference the `customizationSpec` and use [cloud template conditional syntax](https://docs.vmware.com/en/vRealize-Automation/8.4/Using-and-Managing-Cloud-Assembly/GUID-12F0BC64-6391-4E5F-AA48-C5959024F3EB.html#conditions-4) to apply the correct spec based on whether it's a domain or workgroup deployment. (These conditionals take the pattern `'${conditional-expresion ? true-value : false-value}'`).
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
resources:
Cloud_vSphere_Machine_1:
type: Cloud.vSphere.Machine
@ -81,7 +83,8 @@ resources:
Here's the current cloud template in its entirety:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
formatVersion: 1
inputs:
site:

View file

@ -54,8 +54,8 @@ Sounds pretty cool, right? I'm not going to go too deep into "how to Packer" in
## Prerequisites
### Install Packer
Before being able to *use* Packer, you have to install it. On Debian/Ubuntu Linux, this process consists of adding the HashiCorp GPG key and software repository, and then simply installing the package:
```command
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
```shell
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - # [tl! .cmd:2]
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install packer
```
@ -113,7 +113,8 @@ Let's quickly run through that build process, and then I'll back up and examine
### `ubuntu-k8s.pkr.hcl`
#### `packer` block
The first block in the file tells Packer about the minimum version requirements for Packer as well as the external plugins used for the build:
``` {linenos=true}
```hcl
// torchlight! {"lineNumbers": true}
// BLOCK: packer
// The Packer configuration.
packer {
@ -134,7 +135,8 @@ As I mentioned above, I'll be using the official [`vsphere` plugin](https://gith
#### `data` block
This section would be used for loading information from various data sources, but I'm only using it for the `sshkey` plugin (as mentioned above).
``` {linenos=true}
```hcl
// torchlight! {"lineNumbers": true}
// BLOCK: data
// Defines data sources.
data "sshkey" "install" {
@ -147,7 +149,8 @@ This will generate an ECDSA keypair, and the public key will include the identif
#### `locals` block
Locals are a type of Packer variable which aren't explicitly declared in the `variables.pkr.hcl` file. They only exist within the context of a single build (hence the "local" name). Typical Packer variables are static and don't support string manipulation; locals, however, do support expressions that can be used to change their value on the fly. This makes them very useful when you need to combine variables into a single string or concatenate lists of SSH public keys (such as in the highlighted lines):
```text {linenos=true,hl_lines=[10,17]}
```hcl
// torchlight! {"lineNumbers": true}
// BLOCK: locals
// Defines local variables.
locals {
@ -182,7 +185,8 @@ The `source` block tells the `vsphere-iso` builder how to connect to vSphere, wh
You'll notice that most of this is just mapping user-defined variables (with the `var.` prefix) to properties used by `vsphere-iso`:
```text {linenos=true}
```hcl
// torchlight! {"lineNumbers": true}
// BLOCK: source
// Defines the builder configuration blocks.
source "vsphere-iso" "ubuntu-k8s" {
@ -284,7 +288,8 @@ source "vsphere-iso" "ubuntu-k8s" {
#### `build` block
This block brings everything together and executes the build. It calls the `source.vsphere-iso.ubuntu-k8s` block defined above, and also ties in a `file` and a few `shell` provisioners. `file` provisioners are used to copy files (like SSL CA certificates) into the VM, while the `shell` provisioners run commands and execute scripts. Those will be handy for the post-deployment configuration tasks, like updating and installing packages.
```text {linenos=true}
```hcl
// torchlight! {"lineNumbers": true}
// BLOCK: build
// Defines the builders to run, provisioners, and post-processors.
build {
@ -323,7 +328,8 @@ Before looking at the build-specific variable definitions, let's take a quick lo
Most of these carry descriptions with them so I won't restate them outside of the code block here:
```text {linenos=true}
```hcl
// torchlight! {"lineNumbers": true}
/*
DESCRIPTION:
Ubuntu Server 20.04 LTS variables using the Packer Builder for VMware vSphere (vsphere-iso).
@ -724,7 +730,8 @@ The full `variables.pkr.hcl` can be viewed [here](https://github.com/jbowdre/vsp
Packer automatically knows to load variables defined in files ending in `*.auto.pkrvars.hcl`. Storing the variable values separately from the declarations in `variables.pkr.hcl` makes it easier to protect sensitive values.
So I'll start by telling Packer what credentials to use for connecting to vSphere, and what vSphere resources to deploy to:
```text {linenos=true}
```hcl
// torchlight! {"lineNumbers": true}
/*
DESCRIPTION:
Ubuntu Server 20.04 LTS Kubernetes node variables used by the Packer Plugin for VMware vSphere (vsphere-iso).
@ -745,7 +752,8 @@ vsphere_folder = "_Templates"
```
I'll then describe the properties of the VM itself:
```text {linenos=true}
```hcl
// torchlight! {"lineNumbers": true}
// Guest Operating System Settings
vm_guest_os_language = "en_US"
vm_guest_os_keyboard = "us"
@ -771,7 +779,8 @@ common_remove_cdrom = true
```
Then I'll configure Packer to convert the VM to a template once the build is finished:
```text {linenos=true}
```hcl
// torchlight! {"lineNumbers": true}
// Template and Content Library Settings
common_template_conversion = true
common_content_library_name = null
@ -786,7 +795,8 @@ common_ovf_export_path = ""
```
Next, I'll tell it where to find the Ubuntu 20.04 ISO I downloaded and placed on a datastore, along with the SHA256 checksum to confirm its integrity:
```text {linenos=true}
```hcl
// torchlight! {"lineNumbers": true}
// Removable Media Settings
common_iso_datastore = "nuchost-local"
iso_url = null
@ -797,7 +807,8 @@ iso_checksum_value = "5035be37a7e9abbdc09f0d257f3e33416c1a0fb322ba860d42d74
```
And then I'll specify the VM's boot device order, as well as the boot command that will be used for loading the `cloud-init` coniguration into the Ubuntu installer:
```text {linenos=true}
```hcl
// torchlight! {"lineNumbers": true}
// Boot Settings
vm_boot_order = "disk,cdrom"
vm_boot_wait = "4s"
@ -814,7 +825,8 @@ vm_boot_command = [
Once the installer is booted and running, Packer will wait until the VM is available via SSH and then use these credentials to log in. (How will it be able to log in with those creds? We'll take a look at the `cloud-init` configuration in just a minute...)
```text {linenos=true}
```hcl
// torchlight! {"lineNumbers": true}
// Communicator Settings
communicator_port = 22
communicator_timeout = "20m"
@ -832,7 +844,8 @@ ssh_keys = [
Finally, I'll create two lists of scripts that will be run on the VM once the OS install is complete. The `post_install_scripts` will be run immediately after the operating system installation. The `update-packages.sh` script will cause a reboot, and then the set of `pre_final_scripts` will do some cleanup and prepare the VM to be converted to a template.
The last bit of this file also designates the desired version of Kubernetes to be installed.
```text {linenos=true}
```hcl
// torchlight! {"lineNumbers": true}
// Provisioner Settings
post_install_scripts = [
"scripts/wait-for-cloud-init.sh",
@ -864,7 +877,8 @@ Okay, so we've covered the Packer framework that creates the VM; now let's take
See the bits that look `${ like_this }`? Those place-holders will take input from the [`locals` block of `ubuntu-k8s.pkr.hcl`](#locals-block) mentioned above. So that's how all the OS properties will get set, including the hostname, locale, LVM partition layout, username, password, and SSH keys.
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
#cloud-config
autoinstall:
version: 1
@ -899,7 +913,7 @@ autoinstall:
%{ endfor ~}
%{ endif ~}
storage:
config:
config: # [tl! collapse:start]
- ptable: gpt
path: /dev/sda
wipe: superblock
@ -1037,7 +1051,7 @@ autoinstall:
- path: /var/log/audit
device: format-audit
type: mount
id: mount-audit
id: mount-audit # [tl! collapse:end]
user-data:
package_upgrade: true
disable_root: true
@ -1068,7 +1082,8 @@ You can find all of the scripts [here](https://github.com/jbowdre/vsphere-k8s/tr
#### `wait-for-cloud-init.sh`
This simply holds up the process until the `/var/lib/cloud//instance/boot-finished` file has been created, signifying the completion of the `cloud-init` process:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash -eu
echo '>> Waiting for cloud-init...'
while [ ! -f /var/lib/cloud/instance/boot-finished ]; do
@ -1078,7 +1093,8 @@ done
#### `cleanup-subiquity.sh`
Next I clean up any network configs that may have been created during the install process:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash -eu
if [ -f /etc/cloud/cloud.cfg.d/99-installer.cfg ]; then
sudo rm /etc/cloud/cloud.cfg.d/99-installer.cfg
@ -1093,7 +1109,8 @@ fi
#### `install-ca-certs.sh`
The [`file` provisioner](#build-block) mentioned above helpfully copied my custom CA certs to the `/tmp/certs/` folder on the VM; this script will install them into the certificate store:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash -eu
echo '>> Installing custom certificates...'
sudo cp /tmp/certs/* /usr/local/share/ca-certificates/
@ -1106,7 +1123,8 @@ sudo /usr/sbin/update-ca-certificates
#### `disable-multipathd.sh`
This disables `multipathd`:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash -eu
sudo systemctl disable multipathd
echo 'Disabling multipathd'
@ -1114,7 +1132,8 @@ echo 'Disabling multipathd'
#### `disable-release-upgrade-motd.sh`
And this one disable the release upgrade notices that would otherwise be displayed upon each login:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash -eu
echo '>> Disabling release update MOTD...'
sudo chmod -x /etc/update-motd.d/91-release-upgrade
@ -1122,7 +1141,8 @@ sudo chmod -x /etc/update-motd.d/91-release-upgrade
#### `persist-cloud-init-net.sh`
I want to make sure that this VM keeps the same IP address following the reboot that will come in a few minutes, so I 'll set a quick `cloud-init` option to help make sure that happens:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/sh -eu
echo '>> Preserving network settings...'
echo 'manual_cache_clean: True' | sudo tee -a /etc/cloud/cloud.cfg
@ -1131,7 +1151,8 @@ echo 'manual_cache_clean: True' | sudo tee -a /etc/cloud/cloud.cfg
#### `configure-sshd.sh`
Then I just set a few options for the `sshd` configuration, like disabling root login:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash -eu
echo '>> Configuring SSH'
sudo sed -i 's/.*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
@ -1143,7 +1164,8 @@ sudo sed -i 's/.*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/
This script is a little longer and takes care of all the Kubernetes-specific settings and packages that will need to be installed on the VM.
First I enable the required `overlay` and `br_netfilter` modules:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash -eu
echo ">> Installing Kubernetes components..."
@ -1159,7 +1181,8 @@ sudo modprobe br_netfilter
```
Then I'll make some networking tweaks to enable forwarding and bridging:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
# Configure networking
echo ".. configure networking"
cat << EOF | sudo tee /etc/sysctl.d/99-kubernetes-cri.conf
@ -1172,7 +1195,8 @@ sudo sysctl --system
```
Next, set up `containerd` as the container runtime:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
# Setup containerd
echo ".. setup containerd"
sudo apt-get update && sudo apt-get install -y containerd apt-transport-https jq
@ -1182,7 +1206,8 @@ sudo systemctl restart containerd
```
Then disable swap:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
# Disable swap
echo ".. disable swap"
sudo sed -i '/[[:space:]]swap[[:space:]]/ s/^\(.*\)$/#\1/g' /etc/fstab
@ -1190,7 +1215,8 @@ sudo swapoff -a
```
Next I'll install the Kubernetes components and (crucially) `apt-mark hold` them so they won't be automatically upgraded without it being a coordinated change:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
# Install Kubernetes
echo ".. install kubernetes version ${KUBEVERSION}"
sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg
@ -1201,7 +1227,8 @@ sudo apt-mark hold kubelet kubeadm kubectl
#### `update-packages.sh`
Lastly, I'll be sure to update all installed packages (excepting the Kubernetes ones, of course), and then perform a reboot to make sure that any new kernel modules get loaded:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash -eu
echo '>> Checking for and installing updates...'
sudo apt-get update && sudo apt-get -y upgrade
@ -1214,7 +1241,8 @@ After the reboot, all that's left are some cleanup tasks to get the VM ready to
#### `cleanup-cloud-init.sh`
I'll start with cleaning up the `cloud-init` state:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash -eu
echo '>> Cleaning up cloud-init state...'
sudo cloud-init clean -l
@ -1222,7 +1250,8 @@ sudo cloud-init clean -l
#### `enable-vmware-customization.sh`
And then be (re)enable the ability for VMware to be able to customize the guest successfully:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash -eu
echo '>> Enabling legacy VMware Guest Customization...'
echo 'disable_vmware_customization: true' | sudo tee -a /etc/cloud/cloud.cfg
@ -1231,7 +1260,8 @@ sudo vmware-toolbox-cmd config set deployPkg enable-custom-scripts true
#### `zero-disk.sh`
I'll also execute this handy script to free up unused space on the virtual disk. It works by creating a file which completely fills up the disk, and then deleting that file:
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash -eu
echo '>> Zeroing free space to reduce disk size'
sudo sh -c 'dd if=/dev/zero of=/EMPTY bs=1M || true; sync; sleep 1; sync'
@ -1240,7 +1270,8 @@ sudo sh -c 'rm -f /EMPTY; sync; sleep 1; sync'
#### `generalize.sh`
Lastly, let's do a final run of cleaning up logs, temporary files, and unique identifiers that don't need to exist in a template. This script will also remove the SSH key with the `packer_key` identifier since that won't be needed anymore.
```shell {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash -eu
# Prepare a VM to become a template.
@ -1293,8 +1324,8 @@ sudo rm -f /root/.bash_history
### Kick out the jams (or at least the build)
Now that all the ducks are nicely lined up, let's give them some marching orders and see what happens. All I have to do is open a terminal session to the folder containing the `.pkr.hcl` files, and then run the Packer build command:
```command
packer packer build -on-error=abort -force .
```shell
packer packer build -on-error=abort -force . # [tl! .cmd]
```
{{% notice note "Flags" %}}

View file

@ -77,6 +77,7 @@ I can then click through the rest of the wizard but (as before) I'll stop on the
#### Editing the cluster spec
Remember that awkward `member:1.2.840.113556.1.4.1941:` attribute from earlier? Here's how it looks within the TCE cluster-defining YAML:
```yaml
# torchlight! {"lineNumbers": true}
LDAP_GROUP_SEARCH_BASE_DN: OU=LAB,DC=lab,DC=bowdre,DC=net
LDAP_GROUP_SEARCH_FILTER: (objectClass=group)
LDAP_GROUP_SEARCH_GROUP_ATTRIBUTE: 'member:1.2.840.113556.1.4.1941:'
@ -86,24 +87,27 @@ LDAP_GROUP_SEARCH_USER_ATTRIBUTE: DN
That `:` at the end of the line will cause problems down the road - specifically when the deployment process creates the `dex` app which handles the actual LDAPS authentication piece. Cumulative hours of [troubleshooting](#troubleshooting-notes) (and learning!) eventually revealed to me that something along the way had choked on that trailing colon and inserted this into the `dex` configuration:
```yaml
# torchlight! {"lineNumbers": true}
userMatchers:
- userAttr: DN
groupAttr:
member:1.2.840.113556.1.4.1941: null
member:1.2.840.113556.1.4.1941: null # [tl! focus]
```
It *should* look like this instead:
```yaml
# torchlight! {"lineNumbers": true}
userMatchers:
- userAttr: DN
groupAttr: 'member:1.2.840.113556.1.4.1941:'
groupAttr: 'member:1.2.840.113556.1.4.1941:' # [tl! focus]
```
That error prevents `dex` from starting correctly so the authentication would never work. I eventually figured out that using the `|` character to define the attribute as a [literal scalar](https://yaml.org/spec/1.2.2/#812-literal-style) would help to get around this issue so I changed the cluster YAML to look like this:
```yaml
# torchlight! {"lineNumbers": true}
LDAP_GROUP_SEARCH_BASE_DN: OU=LAB,DC=lab,DC=bowdre,DC=net
LDAP_GROUP_SEARCH_FILTER: (objectClass=group)
LDAP_GROUP_SEARCH_GROUP_ATTRIBUTE: |
LDAP_GROUP_SEARCH_GROUP_ATTRIBUTE: | # [tl! focus:1]
'member:1.2.840.113556.1.4.1941:'
LDAP_GROUP_SEARCH_NAME_ATTRIBUTE: cn
LDAP_GROUP_SEARCH_USER_ATTRIBUTE: DN
@ -113,8 +117,8 @@ LDAP_GROUP_SEARCH_USER_ATTRIBUTE: DN
#### Deploying the cluster
That's the only thing I need to manually edit so now I can go ahead and create the cluster with:
```command
tanzu management-cluster create tce-mgmt -f tce-mgmt-deploy.yaml
```shell
tanzu management-cluster create tce-mgmt -f tce-mgmt-deploy.yaml # [tl! .cmd]
```
This will probably take 10-15 minutes to deploy so it's a great time to go top off my coffee.
@ -136,20 +140,19 @@ Some addons might be getting installed! Check their status by running the follow
```
I obediently follow the instructions to switch to the correct context and verify that the addons are all running:
```command-session
kubectl config use-context tce-mgmt-admin@tce-mgmt
Switched to context "tce-mgmt-admin@tce-mgmt".
```
```command-session
kubectl get apps -A
NAMESPACE NAME DESCRIPTION SINCE-DEPLOY AGE
```shell
kubectl config use-context tce-mgmt-admin@tce-mgmt # [tl! .cmd]
Switched to context "tce-mgmt-admin@tce-mgmt". # [tl! .nocopy:1]
kubectl get apps -A # [tl! .cmd]
NAMESPACE NAME DESCRIPTION SINCE-DEPLOY AGE # [tl! .nocopy:start]
tkg-system antrea Reconcile succeeded 5m2s 11m
tkg-system metrics-server Reconcile succeeded 39s 11m
tkg-system pinniped Reconcile succeeded 4m55s 11m
tkg-system secretgen-controller Reconcile succeeded 65s 11m
tkg-system tanzu-addons-manager Reconcile succeeded 70s 11m
tkg-system vsphere-cpi Reconcile succeeded 32s 11m
tkg-system vsphere-csi Reconcile succeeded 66s 11m
tkg-system vsphere-csi Reconcile succeeded 66s 11m # [tl! .nocopy:end]
```
### Post-deployment tasks
@ -159,25 +162,24 @@ I've got a TCE cluster now but it's not quite ready for me to authenticate with
#### Load Balancer deployment
The [guide I'm following from the TCE site](https://tanzucommunityedition.io/docs/latest/vsphere-ldap-config/) assumes that I'm using NSX-ALB in my environment, but I'm not. So, [as before](/tanzu-community-edition-k8s-homelab/#deploying-kube-vip-as-a-load-balancer), I'll need to deploy [Scott Rosenberg's `kube-vip` Carvel package](https://github.com/vrabbi/tkgm-customizations):
```command
git clone https://github.com/vrabbi/tkgm-customizations.git
```shell
git clone https://github.com/vrabbi/tkgm-customizations.git # [tl! .cmd:3]
cd tkgm-customizations/carvel-packages/kube-vip-package
kubectl apply -n tanzu-package-repo-global -f metadata.yml
kubectl apply -n tanzu-package-repo-global -f package.yaml
```
```command-session
cat << EOF > values.yaml
cat << EOF > values.yaml # [tl! .cmd]
vip_range: 192.168.1.64-192.168.1.70
EOF
```
```command
tanzu package install kubevip -p kubevip.terasky.com -v 0.3.9 -f values.yaml
tanzu package install kubevip -p kubevip.terasky.com -v 0.3.9 -f values.yaml # [tl! .cmd]
```
#### Modifying services to use the Load Balancer
With the load balancer in place, I can follow the TCE instruction to modify the Pinniped and Dex services to switch from the `NodePort` type to the `LoadBalancer` type so they can be easily accessed from outside of the cluster. This process starts by creating a file called `pinniped-supervisor-svc-overlay.yaml` and pasting in the following overlay manifest:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
#@ load("@ytt:overlay", "overlay")
#@overlay/match by=overlay.subset({"kind": "Service", "metadata": {"name": "pinniped-supervisor", "namespace": "pinniped-supervisor"}})
---
@ -208,42 +210,42 @@ spec:
```
This overlay will need to be inserted into the `pinniped-addon` secret which means that the contents need to be converted to a base64-encoded string:
```command-session
base64 -w 0 pinniped-supervisor-svc-overlay.yaml
I0AgbG9hZCgi[...]==
```shell
base64 -w 0 pinniped-supervisor-svc-overlay.yaml # [tl! .cmd]
I0AgbG9hZCgi[...]== # [tl! .nocopy]
```
{{% notice note "Avoid newlines" %}}
The `-w 0` / `--wrap=0` argument tells `base64` to *not* wrap the encoded lines after a certain number of characters. If you leave this off, the string will get a newline inserted every 76 characters, and those linebreaks would make the string a bit more tricky to work with. Avoid having to clean up the output afterwards by being more specific with the request up front!
{{% /notice %}}
I'll copy the resulting base64 string (which is much longer than the truncated form I'm using here), and paste it into the following command to patch the secret (which will be named after the management cluster name so replace the `tce-mgmt` part as appropriate):
```command-session
kubectl -n tkg-system patch secret tce-mgmt-pinniped-addon -p '{"data": {"overlays.yaml": "I0AgbG9hZCgi[...]=="}}'
secret/tce-mgmt-pinniped-addon patched
```shell
kubectl -n tkg-system patch secret tce-mgmt-pinniped-addon -p '{"data": {"overlays.yaml": "I0AgbG9hZCgi[...]=="}}' # [tl! .cmd]
secret/tce-mgmt-pinniped-addon patched # [tl! .nocopy]
```
I can watch as the `pinniped-supervisor` and `dexsvc` services get updated with the new service type:
```command-session
kubectl get svc -A -w
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
```shell
kubectl get svc -A -w # [tl! .cmd]
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) # [tl! .nocopy:start]
pinniped-supervisor pinniped-supervisor NodePort 100.65.185.82 <none> 443:31234/TCP
tanzu-system-auth dexsvc NodePort 100.70.238.106 <none> 5556:30167/TCP
tkg-system packaging-api ClusterIP 100.65.185.94 <none> 443/TCP
tanzu-system-auth dexsvc LoadBalancer 100.70.238.106 <pending> 443:30167/TCP
pinniped-supervisor pinniped-supervisor LoadBalancer 100.65.185.82 <pending> 443:31234/TCP
pinniped-supervisor pinniped-supervisor LoadBalancer 100.65.185.82 192.168.1.70 443:31234/TCP
tanzu-system-auth dexsvc LoadBalancer 100.70.238.106 192.168.1.64 443:30167/TCP
tanzu-system-auth dexsvc LoadBalancer 100.70.238.106 192.168.1.64 443:30167/TCP # [tl! .nocopy:end]
```
I'll also need to restart the `pinniped-post-deploy-job` job to account for the changes I just made; that's accomplished by simply deleting the existing job. After a few minutes a new job will be spawned automagically. I'll just watch for the new job to be created:
```command-session
kubectl -n pinniped-supervisor delete jobs pinniped-post-deploy-job
job.batch "pinniped-post-deploy-job" deleted
```shell
kubectl -n pinniped-supervisor delete jobs pinniped-post-deploy-job # [tl! .cmd]
job.batch "pinniped-post-deploy-job" deleted # [tl! .nocopy]
```
```command-session
kubectl get jobs -A -w
NAMESPACE NAME COMPLETIONS DURATION AGE
```shell
kubectl get jobs -A -w # [tl! cmd]
NAMESPACE NAME COMPLETIONS DURATION AGE # [tl! .nocopy:4]
pinniped-supervisor pinniped-post-deploy-job 0/1 0s
pinniped-supervisor pinniped-post-deploy-job 0/1 0s
pinniped-supervisor pinniped-post-deploy-job 0/1 0s 0s
@ -254,7 +256,8 @@ pinniped-supervisor pinniped-post-deploy-job 1/1 9s 9s
Right now, I've got all the necessary components to support LDAPS authentication with my TCE management cluster but I haven't done anything yet to actually define who should have what level of access. To do that, I'll create a `ClusterRoleBinding`.
I'll toss this into a file I'll call `tanzu-admins-crb.yaml`:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
@ -274,25 +277,24 @@ I have a group in Active Directory called `Tanzu-Admins` which contains a group
Once applied, users within that group will be granted the `cluster-admin` role[^roles].
Let's do it:
```command-session
kubectl apply -f tanzu-admins-crb.yaml
clusterrolebinding.rbac.authorization.k8s.io/tanzu-admins created
```shell
kubectl apply -f tanzu-admins-crb.yaml # [tl! .cmd]
clusterrolebinding.rbac.authorization.k8s.io/tanzu-admins created # [tl! .nocopy]
```
Thus far, I've been using the default administrator context to interact with the cluster. Now it's time to switch to the non-admin context:
```command-session
tanzu management-cluster kubeconfig get
You can now access the cluster by running 'kubectl config use-context tanzu-cli-tce-mgmt@tce-mgmt'
```
```command-session
kubectl config use-context tanzu-cli-tce-mgmt@tce-mgmt
Switched to context "tanzu-cli-tce-mgmt@tce-mgmt".
```shell
tanzu management-cluster kubeconfig get # [tl! .cmd]
You can now access the cluster by running 'kubectl config use-context tanzu-cli-tce-mgmt@tce-mgmt' # [tl! .nocopy:1]
kubectl config use-context tanzu-cli-tce-mgmt@tce-mgmt # [tl! .cmd]
Switched to context "tanzu-cli-tce-mgmt@tce-mgmt". # [tl! .nocopy]
```
After assuming the non-admin context, the next time I try to interact with the cluster it should kick off the LDAPS authentication process. It won't look like anything is happening in the terminal:
```command-session
kubectl get nodes
```shell
kubectl get nodes # [tl! .cmd]
# [tl! .nocopy]
```
But it will shortly spawn a browser page prompting me to log in:
@ -302,9 +304,9 @@ Doing so successfully will yield:
![Dex login success!](dex_login_success.png)
And the `kubectl` command will return the expected details:
```command-session
kubectl get nodes
NAME STATUS ROLES AGE VERSION
```shell
kubectl get nodes # [tl! .cmd]
NAME STATUS ROLES AGE VERSION # [tl! .nocopy:2]
tce-mgmt-control-plane-v8l8r Ready control-plane,master 29h v1.21.5+vmware.1
tce-mgmt-md-0-847db9ddc-5bwjs Ready <none> 28h v1.21.5+vmware.1
```
@ -326,9 +328,9 @@ Other users hoping to work with a Tanzu Community Edition cluster will also need
At this point, I've only configured authentication for the management cluster - not the workload cluster. The TCE community docs cover what's needed to make this configuration available in the workload cluster as well [here](https://tanzucommunityedition.io/docs/latest/vsphere-ldap-config/#configuration-steps-on-the-workload-cluster). [As before](/tanzu-community-edition-k8s-homelab/#workload-cluster), I created the deployment YAML for the workload cluster by copying the management cluster's deployment YAML and changing the `CLUSTER_NAME` and `VSPHERE_CONTROL_PLANE_ENDPOINT` values accordingly. This time I also deleted all of the `LDAP_*` and `OIDC_*` lines, but made sure to preserve the `IDENTITY_MANAGEMENT_TYPE: ldap` one.
I was then able to deploy the workload cluster with:
```command-session
tanzu cluster create --file tce-work-deploy.yaml
Validating configuration...
```shell
tanzu cluster create --file tce-work-deploy.yaml # [tl! .cmd]
Validating configuration... # [tl! .nocopy:start]
Creating workload cluster 'tce-work'...
Waiting for cluster to be initialized...
cluster control plane is still being initialized: WaitingForControlPlane
@ -337,38 +339,35 @@ Waiting for cluster nodes to be available...
Waiting for addons installation...
Waiting for packages to be up and running...
Workload cluster 'tce-work' created
Workload cluster 'tce-work' created # [tl! .nocopy:end]
```
Access the admin context:
```command-session
tanzu cluster kubeconfig get --admin tce-work
Credentials of cluster 'tce-work' have been saved
```shell
tanzu cluster kubeconfig get --admin tce-work # [tl! .cmd]
Credentials of cluster 'tce-work' have been saved # [tl! .nocopy:2]
You can now access the cluster by running 'kubectl config use-context tce-work-admin@tce-work'
```
```command-session
kubectl config use-context tce-work-admin@tce-work
Switched to context "tce-work-admin@tce-work".
kubectl config use-context tce-work-admin@tce-work # [tl! .cmd]
Switched to context "tce-work-admin@tce-work". # [tl! .nocopy]
```
Apply the same ClusterRoleBinding from before[^crb]:
```command-session
kubectl apply -f tanzu-admins-crb.yaml
clusterrolebinding.rbac.authorization.k8s.io/tanzu-admins created
```shell
kubectl apply -f tanzu-admins-crb.yaml # [tl! .cmd]
clusterrolebinding.rbac.authorization.k8s.io/tanzu-admins created # [tl! .nocopy]
```
And finally switch to the non-admin context and log in with my AD account:
```command-session
tanzu cluster kubeconfig get tce-work
You can now access the cluster by running 'kubectl config use-context tanzu-cli-tce-work@tce-work'
```
```command-session
kubectl config use-context tanzu-cli-tce-work@tce-work
Switched to context "tanzu-cli-tce-work@tce-work".
```
```command-session
kubectl get nodes
NAME STATUS ROLES AGE VERSION
```shell
tanzu cluster kubeconfig get tce-work # [tl! .cmd]
You can now access the cluster by running 'kubectl config use-context tanzu-cli-tce-work@tce-work' # [tl! .nocopy:1]
kubectl config use-context tanzu-cli-tce-work@tce-work # [tl! .cmd]
Switched to context "tanzu-cli-tce-work@tce-work". # [tl! .nocopy:1]
kubectl get nodes # [tl! .cmd]
NAME STATUS ROLES AGE VERSION # [tl! .nocopy:2]
tce-work-control-plane-zts6r Ready control-plane,master 12m v1.21.5+vmware.1
tce-work-md-0-bcfdc4d79-vn9xb Ready <none> 11m v1.21.5+vmware.1
```
@ -387,9 +386,9 @@ It took me quite a bit of trial and error to get this far and (being a k8s novic
#### Checking and modifying `dex` configuration
I had a lot of trouble figuring out how to correctly format the `member:1.2.840.113556.1.4.1941:` attribute in the LDAPS config so that it wouldn't get split into multiple attributes due to the trailing colon - and it took me forever to discover that was even the issue. What eventually did the trick for me was learning that I could look at (and modify!) the configuration for the `dex` app with:
```command-session
kubectl -n tanzu-system-auth edit configmaps dex
[...]
```shell
kubectl -n tanzu-system-auth edit configmaps dex # [tl! .cmd]
[...] # [tl! .nocopy:start]
groupSearch:
baseDN: OU=LAB,DC=lab,DC=bowdre,DC=net
filter: (objectClass=group)
@ -399,7 +398,7 @@ kubectl -n tanzu-system-auth edit configmaps dex
- userAttr: DN
groupAttr: 'member:1.2.840.113556.1.4.1941:'
host: win01.lab.bowdre.net:636
[...]
[...] # [tl! .nocopy:end]
```
This let me make changes on the fly until I got a working configuration and then work backwards from there to format the initial input correctly.
@ -407,14 +406,13 @@ This let me make changes on the fly until I got a working configuration and then
#### Reviewing `dex` logs
Authentication attempts (at least on the LDAPS side of things) will show up in the logs for the `dex` pod running in the `tanzu-system-auth` namespace. This is a great place to look to see if the user isn't being found, credentials are invalid, or the groups aren't being enumerated correctly:
```command-session
kubectl -n tanzu-system-auth get pods
NAME READY STATUS RESTARTS AGE
```shell
kubectl -n tanzu-system-auth get pods # [tl! .cmd]
NAME READY STATUS RESTARTS AGE # [tl! .nocopy:2]
dex-7bf4f5d4d9-k4jfl 1/1 Running 0 40h
```
```command-session
kubectl -n tanzu-system-auth logs dex-7bf4f5d4d9-k4jfl
# no such user
kubectl -n tanzu-system-auth logs dex-7bf4f5d4d9-k4jfl # [tl! .cmd]
# no such user # [tl! .nocopy:start]
{"level":"info","msg":"performing ldap search OU=LAB,DC=lab,DC=bowdre,DC=net sub (\u0026(objectClass=person)(sAMAccountName=johnny))","time":"2022-03-06T22:29:57Z"}
{"level":"error","msg":"ldap: no results returned for filter: \"(\u0026(objectClass=person)(sAMAccountName=johnny))\"","time":"2022-03-06T22:29:57Z"}
#invalid password
@ -425,15 +423,15 @@ kubectl -n tanzu-system-auth logs dex-7bf4f5d4d9-k4jfl
{"level":"info","msg":"performing ldap search OU=LAB,DC=lab,DC=bowdre,DC=net sub (\u0026(objectClass=person)(sAMAccountName=john))","time":"2022-03-06T22:31:21Z"}
{"level":"info","msg":"username \"john\" mapped to entry CN=John Bowdre,OU=Users,OU=BOW,OU=LAB,DC=lab,DC=bowdre,DC=net","time":"2022-03-06T22:31:21Z"}
{"level":"info","msg":"performing ldap search OU=LAB,DC=lab,DC=bowdre,DC=net sub (\u0026(objectClass=group)(member:1.2.840.113556.1.4.1941:=CN=John Bowdre,OU=Users,OU=BOW,OU=LAB,DC=lab,DC=bowdre,DC=net))","time":"2022-03-06T22:31:21Z"}
{"level":"info","msg":"login successful: connector \"ldap\", username=\"john\", preferred_username=\"\", email=\"CN=John Bowdre,OU=Users,OU=BOW,OU=LAB,DC=lab,DC=bowdre,DC=net\", groups=[\"vRA-Admins\" \"Tanzu-Admins\"]","time":"2022-03-06T22:31:21Z"}
{"level":"info","msg":"login successful: connector \"ldap\", username=\"john\", preferred_username=\"\", email=\"CN=John Bowdre,OU=Users,OU=BOW,OU=LAB,DC=lab,DC=bowdre,DC=net\", groups=[\"vRA-Admins\" \"Tanzu-Admins\"]","time":"2022-03-06T22:31:21Z"} # [tl! .nocopy:end]
```
#### Clearing pinniped sessions
I couldn't figure out an elegant way to log out so that I could try authenticating as a different user, but I did discover that information about authenticated sessions get stored in `~/.config/tanzu/pinniped/sessions.yaml`. The sessions expired after a while but until that happens I'm able to keep on interacting with `kubectl` - and not given an option to re-authenticate even if I wanted to.
So in lieu of a handy logout option, I was able to remove the cached sessions by deleting the file:
```command
rm ~/.config/tanzu/pinniped/sessions.yaml
```shell
rm ~/.config/tanzu/pinniped/sessions.yaml # [tl! .cmd]
```
That let me use `kubectl get nodes` to trigger the authentication prompt again.

View file

@ -24,17 +24,18 @@ comment: true # Disable comment if false.
When I [set up my Tanzu Community Edition environment](/tanzu-community-edition-k8s-homelab/), I did so from a Linux VM since the containerized Linux environment on my Chromebook doesn't support the `kind` bootstrap cluster used for the deployment. But now that the Kubernetes cluster is up and running, I'd like to be able to connect to it directly without the aid of a jumpbox. How do I get the appropriate cluster configuration over to my Chromebook?
The Tanzu CLI actually makes that pretty easy - once I figured out the appropriate incantation. I just needed to use the `tanzu management-cluster kubeconfig get` command on my Linux VM to export the `kubeconfig` of my management (`tce-mgmt`) cluster to a file:
```command
tanzu management-cluster kubeconfig get --admin --export-file tce-mgmt-kubeconfig.yaml
```shell
tanzu management-cluster kubeconfig get --admin --export-file tce-mgmt-kubeconfig.yaml # [tl! .cmd]
```
I then used `scp` to pull the file from the VM into my local Linux environment, and proceeded to [install `kubectl`](/tanzu-community-edition-k8s-homelab/#kubectl-binary) and the [`tanzu` CLI](/tanzu-community-edition-k8s-homelab/#tanzu-cli) (making sure to also [enable shell auto-completion](/enable-tanzu-cli-auto-completion-bash-zsh/) along the way!).
Now I'm ready to import the configuration locally with `tanzu login` on my Chromebook:
```command-session
tanzu login --kubeconfig ~/projects/tanzu-homelab/tanzu-setup/tce-mgmt-kubeconfig.yaml --context tce-mgmt-admin@tce-mgmt --name tce-mgmt
✔ successfully logged in to management cluster using the kubeconfig tce-mgmt
```shell
tanzu login --kubeconfig ~/projects/tanzu-homelab/tanzu-setup/tce-mgmt-kubeconfig.yaml \ # [tl! .cmd]
--context tce-mgmt-admin@tce-mgmt --name tce-mgmt
✔ successfully logged in to management cluster using the kubeconfig tce-mgmt # [tl! .nocopy]
```
{{% notice tip "Use the absolute path" %}}
@ -42,14 +43,13 @@ Pass in the full path to the exported kubeconfig file. This will help the Tanzu
{{% /notice %}}
Even though that's just importing the management cluster it actually grants access to both the management and workload clusters:
```command-session
tanzu cluster list
NAME NAMESPACE STATUS CONTROLPLANE WORKERS KUBERNETES ROLES PLAN
```shell
tanzu cluster list # [tl! .cmd]
NAME NAMESPACE STATUS CONTROLPLANE WORKERS KUBERNETES ROLES PLAN # [tl! .nocopy:2]
tce-work default running 1/1 1/1 v1.21.2+vmware.1 <none> dev
```
```command-session
tanzu cluster get tce-work
NAME NAMESPACE STATUS CONTROLPLANE WORKERS KUBERNETES ROLES
tanzu cluster get tce-work # [tl! .cmd]
NAME NAMESPACE STATUS CONTROLPLANE WORKERS KUBERNETES ROLES # [tl! .nocopy:start]
tce-work default running 1/1 1/1 v1.21.2+vmware.1 <none>
@ -63,10 +63,9 @@ NAME READY SEVERITY RE
└─Workers
└─MachineDeployment/tce-work-md-0
└─Machine/tce-work-md-0-687444b744-crc9q True 24h
```
```command-session
tanzu management-cluster get
NAME NAMESPACE STATUS CONTROLPLANE WORKERS KUBERNETES ROLES
# [tl! .nocopy:end]
tanzu management-cluster get # [tl! .cmd]
NAME NAMESPACE STATUS CONTROLPLANE WORKERS KUBERNETES ROLES # [tl! .nocopy:start]
tce-mgmt tkg-system running 1/1 1/1 v1.21.2+vmware.1 management
@ -88,31 +87,29 @@ Providers:
capi-kubeadm-bootstrap-system bootstrap-kubeadm BootstrapProvider kubeadm v0.3.23
capi-kubeadm-control-plane-system control-plane-kubeadm ControlPlaneProvider kubeadm v0.3.23
capi-system cluster-api CoreProvider cluster-api v0.3.23
capv-system infrastructure-vsphere InfrastructureProvider vsphere v0.7.10
capv-system infrastructure-vsphere InfrastructureProvider vsphere v0.7.10 # [tl! .nocopy:end]
```
And I can then tell `kubectl` about the two clusters:
```command-session
tanzu management-cluster kubeconfig get tce-mgmt --admin
Credentials of cluster 'tce-mgmt' have been saved
```shell
tanzu management-cluster kubeconfig get tce-mgmt --admin # [tl! .cmd]
Credentials of cluster 'tce-mgmt' have been saved # [tl! .nocopy:2]
You can now access the cluster by running 'kubectl config use-context tce-mgmt-admin@tce-mgmt'
```
```command-session
tanzu cluster kubeconfig get tce-work --admin
Credentials of cluster 'tce-work' have been saved
tanzu cluster kubeconfig get tce-work --admin # [tl! .cmd]
Credentials of cluster 'tce-work' have been saved # [tl! .nocopy:1]
You can now access the cluster by running 'kubectl config use-context tce-work-admin@tce-work'
```
And sure enough, there are my contexts:
```command-session
kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
```shell
kubectl config get-contexts # [tl! .cmd]
CURRENT NAME CLUSTER AUTHINFO NAMESPACE # [tl! .nocopy:3]
tce-mgmt-admin@tce-mgmt tce-mgmt tce-mgmt-admin
* tce-work-admin@tce-work tce-work tce-work-admin
```
```command-session
kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
kubectl get nodes -o wide # [tl! .cmd]
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME # [tl! .nocopy:2]
tce-work-control-plane-vc2pb Ready control-plane,master 23h v1.21.2+vmware.1 192.168.1.132 192.168.1.132 VMware Photon OS/Linux 4.19.198-1.ph3 containerd://1.4.6
tce-work-md-0-687444b744-crc9q Ready <none> 23h v1.21.2+vmware.1 192.168.1.133 192.168.1.133 VMware Photon OS/Linux 4.19.198-1.ph3 containerd://1.4.6
```

View file

@ -17,7 +17,8 @@ I can, and here's how I do it.
### The Script
The following Powershell script will let you define a list of vCenters to be accessed, securely store your credentials for each vCenter, log in to every vCenter with a single command, and also close the connections when they're no longer needed. It's also a great starting point for any other custom functions you'd like to incorporate into your PowerCLI sessions.
```powershell {linenos=true}
```powershell
# torchlight! {"lineNumbers": true}
# PowerCLI_Custom_Functions.ps1
# Usage:
# 0) Edit $vCenterList to reference the vCenters in your environment.

View file

@ -28,11 +28,11 @@ Now that VMware [has released](https://blogs.vmware.com/vsphere/2022/01/announci
I start off by heading to [tenable.com/products/nessus/nessus-essentials](https://www.tenable.com/products/nessus/nessus-essentials) to register for a (free!) license key which will let me scan up to 16 hosts. I'll receive the key and download link in an email, but I'm not actually going to use that link to download the Nessus binary. I've got this shiny-and-new [Tanzu Community Edition Kubernetes cluster](/tanzu-community-edition-k8s-homelab/) that could use some more real workloads so I'll instead opt for the [Docker version](https://hub.docker.com/r/tenableofficial/nessus).
Tenable provides an [example `docker-compose.yml`](https://community.tenable.com/s/article/Deploy-Nessus-docker-image-with-docker-compose) to make it easy to get started:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
version: '3.1'
services:
nessus:
image: tenableofficial/nessus
restart: always
@ -46,7 +46,8 @@ services:
```
I can use that knowledge to craft something I can deploy on Kubernetes:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
apiVersion: v1
kind: Service
metadata:
@ -95,16 +96,16 @@ spec:
Note that I'm configuring the `LoadBalancer` to listen on port `443` and route traffic to the pod on port `8834` so that I don't have to remember to enter an oddball port number when I want to connect to the web interface.
And now I can just apply the file:
```command-session
kubectl apply -f nessus.yaml
service/nessus created
```shell
kubectl apply -f nessus.yaml # [tl! .cmd]
service/nessus created # [tl! .nocopy:1]
deployment.apps/nessus created
```
I'll give it a moment or two to deploy and then check on the service to figure out what IP I need to use to connect:
```command-session
kubectl get svc/nessus
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
```shell
kubectl get svc/nessus # [tl! .cmd]
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE # [tl! .nocopy:1]
nessus LoadBalancer 100.67.16.51 192.168.1.79 443:31260/TCP 57s
```

View file

@ -28,7 +28,8 @@ I've got a [`Connect-vCenters` function](/logging-in-to-multiple-vcenter-servers
What I came up with is using `Get-Datacenter` to enumerate each virtual datacenter, and then list the VMs matching my query within:
```powershell {linenos=true}
```powershell
# torchlight! {"lineNumbers": true}
$linuxVms = foreach( $datacenter in ( Get-Datacenter )) {
Get-Datacenter $datacenter | Get-VM | Where { $_.ExtensionData.Config.GuestFullName -notmatch "win" -and $_.Name -notmatch "vcls" } | `
Select @{ N="Datacenter";E={ $datacenter.Name }},

View file

@ -23,7 +23,8 @@ comment: true # Disable comment if false.
We've been working lately to use [HashiCorp Packer](https://www.packer.io/) to standardize and automate our VM template builds, and we found a need to pull in all of the contents of a specific directory on an internal web server. This would be pretty simple for Linux systems using `wget -r`, but we needed to find another solution for our Windows builds.
A coworker and I cobbled together a quick PowerShell solution which will download the files within a specified web URL to a designated directory (without recreating the nested folder structure):
```powershell {linenos=true}
```powershell
# torchlight! {"lineNumbers": true}
$outputdir = 'C:\Scripts\Download\'
$url = 'https://win01.lab.bowdre.net/stuff/files/'

View file

@ -20,38 +20,37 @@ Take these steps when you need to snapshot linked vCenters to avoid breaking rep
1. Open an SSH session to *all* the vCenters within the SSO domain.
2. Log in and enter `shell` to access the shell on each vCenter.
3. Verify that replication is healthy by running `/usr/lib/vmware-vmdir/bin/vdcrepadmin -f showpartnerstatus -h localhost -u administrator -w [SSO_ADMIN_PASSWORD]` on each vCenter. You want to ensure that each host shows as available to all other hosts, and the message that `Partner is 0 changes behind.`:
```commandroot-session
/usr/lib/vmware-vmdir/bin/vdcrepadmin -f showpartnerstatus -h localhost -u administrator -w $ssoPass
Partner: vcsa2.lab.bowdre.net
```shell
/usr/lib/vmware-vmdir/bin/vdcrepadmin -f showpartnerstatus -h localhost -u administrator -w $ssoPass # [tl! .cmd]
Partner: vcsa2.lab.bowdre.net # [tl! .nocopy:6]
Host available: Yes
Status available: Yes
My last change number: 9346
Partner has seen my change number: 9346
Partner is 0 changes behind.
```
```commandroot-session
/usr/lib/vmware-vmdir/bin/vdcrepadmin -f showpartnerstatus -h localhost -u administrator -w $ssoPass
Partner: vcsa.lab.bowdre.net
Partner is 0 changes behind. # [tl! highlight]
/usr/lib/vmware-vmdir/bin/vdcrepadmin -f showpartnerstatus -h localhost -u administrator -w $ssoPass # [tl! .cmd]
Partner: vcsa.lab.bowdre.net # [tl! .nocopy:6]
Host available: Yes
Status available: Yes
My last change number: 9518
Partner has seen my change number: 9518
Partner is 0 changes behind.
Partner is 0 changes behind. # [tl! highlight]
```
4. Stop `vmdird` on each vCenter by running `/bin/service-control --stop vmdird`:
```commandroot-session
/bin/service-control --stop vmdird
Operation not cancellable. Please wait for it to finish...
```shell
/bin/service-control --stop vmdird # [tl! .cmd]
Operation not cancellable. Please wait for it to finish... # [tl! .nocopy:2]
Performing stop operation on service vmdird...
Successfully stopped service vmdird
```
5. Snapshot the vCenter appliance VMs.
6. Start replication on each server again with `/bin/service-control --start vmdird`:
```commandroot-session
/bin/service-control --start vmdird
Operation not cancellable. Please wait for it to finish...
```shell
/bin/service-control --start vmdird # [tl! .cmd]
Operation not cancellable. Please wait for it to finish... # [tl! .nocopy]
Performing start operation on service vmdird...
Successfully started service vmdird
```

View file

@ -37,7 +37,8 @@ So yeah. That's, uh, *not great.*
If you've got any **Windows Server 2022** VMs with **[Secure Boot](https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.security.doc/GUID-898217D4-689D-4EB5-866C-888353FE241C.html)** enabled on **ESXi 6.7/7.x**, you'll want to make sure they *do not* get **KB5022842** until this problem is resolved.
I put together a quick PowerCLI query to help identify impacted VMs in my environment:
```powershell {linenos=true}
```powershell
# torchlight! {"lineNumbers": true}
$secureBoot2022VMs = foreach($datacenter in (Get-Datacenter)) {
$datacenter | Get-VM |
Where-Object {$_.Guest.OsFullName -Match 'Microsoft Windows Server 2022' -And $_.ExtensionData.Config.BootOptions.EfiSecureBootEnabled} |

View file

@ -18,7 +18,8 @@ The Jekyll theme I'm using ([Minimal Mistakes](https://github.com/mmistakes/mini
![Posts by category](20210724-posts-by-category.png)
It's a start, though, so I took a few minutes to check out how it's being generated. The category archive page lives at [`_pages/category-archive.md`](https://raw.githubusercontent.com/mmistakes/mm-github-pages-starter/master/_pages/category-archive.md):
```markdown {linenos=true}
```markdown
// torchlight! {"lineNumbers": true}
---
title: "Posts by Category"
layout: categories
@ -30,8 +31,9 @@ author_profile: true
The `title` indicates what's going to be written in bold text at the top of the page, the `permalink` says that it will be accessible at `http://localhost/categories/`, and the nice little `author_profile` sidebar will appear on the left.
This page then calls the `categories` layout, which is defined in [`_layouts/categories.html`](https://github.com/mmistakes/minimal-mistakes/blob/master/_layouts/categories.html):
```liquid {linenos=true}
{% raw %}---
```jinja-html
# torchlight! {"lineNumbers": true}
---
layout: archive
---
@ -81,39 +83,43 @@ I wanted my solution to preserve the formatting that's used by the theme elsewhe
### Defining a new layout
I create a new file called `_layouts/series.html` which will define how these new series pages get rendered. It starts out just like the default `categories.html` one:
```liquid {linenos=true}
{% raw %}---
```jinja-html
# torchlight! {"lineNumbers": true}
---
layout: archive
---
{{ content }}{% endraw %}
{{ content }}
```
That `{{ content }}` block will let me define text to appear above the list of articles - very handy. Much of the original `categories.html` code has to do with iterating through the list of categories. I won't need that, though, so I'll jump straight to setting what layout the entries on this page will use:
```liquid
```jinja-html
{% assign entries_layout = page.entries_layout | default: 'list' %}
```
I'll be including two custom variables in the [Front Matter](https://jekyllrb.com/docs/front-matter/) for my category pages: `tag` to specify what category to filter on, and `sort_order` which will be set to `reverse` if I want the older posts up top. I'll be able to access these in the layout as `page.tag` and `page.sort_order`, respectively. So I'll go ahead and grab all the posts which are categorized with `page.tag`, and then decide whether the posts will get sorted normally or in reverse:
```liquid {linenos=true}
{% raw %}{% assign posts = site.categories[page.tag] %}
```jinja-html
# torchlight! {"lineNumbers": true}
{% assign posts = site.categories[page.tag] %}
{% if page.sort_order == 'reverse' %}
{% assign posts = posts | reverse %}
{% endif %}{% endraw %}
{% endif %}
```
And then I'll loop through each post (in either normal or reverse order) and insert them into the rendered page:
```liquid {linenos=true}
{% raw %}<div class="entries-{{ entries_layout }}">
```jinja-html
# torchlight! {"lineNumbers": true}
<div class="entries-{{ entries_layout }}">
{% for post in posts %}
{% include archive-single.html type=entries_layout %}
{% endfor %}
</div>{% endraw %}
</div>
```
Putting it all together now, here's my new `_layouts/series.html` file:
```liquid {linenos=true}
{% raw %}---
```jinja-html
# torchlight! {"lineNumbers": true}
---
layout: archive
---
@ -133,8 +139,9 @@ layout: archive
### Series pages
Since I can't use a plugin to automatically generate pages for each series, I'll have to do it manually. Fortunately this is pretty easy, and I've got a limited number of categories/series to worry about. I started by making a new `_pages/series-vra8.md` and setting it up thusly:
```markdown {linenos=true}
{% raw %}---
```markdown
// torchlight! {"lineNumbers": true}
---
title: "Adventures in vRealize Automation 8"
layout: series
permalink: "/series/vra8"
@ -145,7 +152,7 @@ header:
teaser: assets/images/posts-2020/RtMljqM9x.png
---
*Follow along as I create a flexible VMware vRealize Automation 8 environment for provisioning virtual machines - all from the comfort of my Intel NUC homelab.*{% endraw %}
*Follow along as I create a flexible VMware vRealize Automation 8 environment for provisioning virtual machines - all from the comfort of my Intel NUC homelab.*
```
You can see that this page is referencing the series layout I just created, and it's going to live at `http://localhost/series/vra8` - precisely where this series was on Hashnode. I've tagged it with the category I want to feature on this page, and specified that the posts will be sorted in reverse order so that anyone reading through the series will start at the beginning (I hear it's a very good place to start). I also added a teaser image which will be displayed when I link to the series from elsewhere. And I included a quick little italicized blurb to tell readers what the series is about.
@ -154,8 +161,9 @@ Check it out [here](/series/vra8):
![vRA8 series](20210724-vra8-series.png)
The other series pages will be basically the same, just without the reverse sort directive. Here's `_pages/series-tips.md`:
```markdown {linenos=true}
{% raw %}---
```markdown
// torchlight! {"lineNumbers": true}
---
title: "Tips & Tricks"
layout: series
permalink: "/series/tips"
@ -165,13 +173,14 @@ header:
teaser: assets/images/posts-2020/kJ_l7gPD2.png
---
*Useful tips and tricks I've stumbled upon.*{% endraw %}
*Useful tips and tricks I've stumbled upon.*
```
### Changing the category permalink
Just in case someone wants to look at all the post series in one place, I'll be keeping the existing category archive page around, but I'll want it to be found at `/series/` instead of `/categories/`. I'll start with going into the `_config.yml` file and changing the `category_archive` path:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
category_archive:
type: liquid
# path: /categories/
@ -182,13 +191,14 @@ tag_archive:
```
I'll also rename `_pages/category-archive.md` to `_pages/series-archive.md` and update its title and permalink:
```markdown {linenos=true}
{% raw %}---
```markdown
// torchlight! {"lineNumbers": true}
---
title: "Posts by Series"
layout: categories
permalink: /series/
author_profile: true
---{% endraw %}
---
```
### Fixing category links in posts
@ -198,30 +208,33 @@ The bottom of each post has a section which lists the tags and categories to whi
That *works* but I'd rather it reference the fancy new pages I created. Tracking down where to make that change was a bit of a journey.
I started with the [`_layouts/single.html`](https://github.com/mmistakes/minimal-mistakes/blob/master/_layouts/single.html) file which is the layout I'm using for individual posts. This bit near the end gave me the clue I needed:
```liquid {linenos=true}
{% raw %} <footer class="page__meta">
```jinja-html
# torchlight! {"lineNumbers": true}
<footer class="page__meta">
{% if site.data.ui-text[site.locale].meta_label %}
<h4 class="page__meta-title">{{ site.data.ui-text[site.locale].meta_label }}</h4>
{% endif %}
{% include page__taxonomy.html %}
{% include page__date.html %}
</footer>{% endraw %}
</footer>
```
It looks like [`page__taxonomy.html`](https://github.com/mmistakes/minimal-mistakes/blob/master/_includes/page__taxonomy.html) is being used to display the tags and categories, so I then went to that file in the `_include` directory:
```liquid {linenos=true}
{% raw %}{% if site.tag_archive.type and page.tags[0] %}
```jinja-html
# torchlight! {"lineNumbers": true}
{% if site.tag_archive.type and page.tags[0] %}
{% include tag-list.html %}
{% endif %}
{% if site.category_archive.type and page.categories[0] %}
{% include category-list.html %}
{% endif %}{% endraw %}
{% endif %}
```
Okay, it looks like [`_include/category-list.html`](https://github.com/mmistakes/minimal-mistakes/blob/master/_includes/category-list.html) is what I actually want. Here's that file:
```liquid {linenos=true}
{% raw %}{% case site.category_archive.type %}
```jinja-html
# torchlight! {"lineNumbers": true}
{% case site.category_archive.type %}
{% when "liquid" %}
{% assign path_type = "#" %}
{% when "jekyll-archives" %}
@ -239,19 +252,21 @@ Okay, it looks like [`_include/category-list.html`](https://github.com/mmistakes
{% endfor %}
</span>
</p>
{% endif %}{% endraw %}
{% endif %}
```
I'm using the `liquid` archive approach since I can't use the `jekyll-archives` plugin, so I can see that it's setting the `path_type` to `"#"`. And near the bottom of the file, I can see that it's assembling the category link by slugifying the `category_word`, sticking the `path_type` in front of it, and then putting the `site.category_archive.path` (which I edited earlier in `_config.yml`) in front of that. So that's why my category links look like `/series/#category`. I can just edit the top of this file to statically set `path_type = nil` and that should clear this up in a jiffy:
```liquid {linenos=true}
{% raw %}{% assign path_type = nil %}
```jinja-html
# torchlight! {"lineNumbers": true}
{% assign path_type = nil %}
{% if site.category_archive.path %}
{% assign categories_sorted = page.categories | sort_natural %}
[...]{% endraw %}
[...]
```
To sell the series illusion even further, I can pop into [`_data/ui-text.yml`](https://github.com/mmistakes/minimal-mistakes/blob/master/_data/ui-text.yml) to update the string used for `categories_label`:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
meta_label :
tags_label : "Tags:"
categories_label : "Series:"
@ -264,7 +279,8 @@ Much better!
### Updating the navigation header
And, finally, I'll want to update the navigation links at the top of each page to help visitors find my new featured series pages. For that, I can just edit `_data/navigation.yml` with links to my new pages:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
main:
- title: "vRealize Automation 8"
url: /series/vra8

View file

@ -29,13 +29,14 @@ I will also add some properties to tell PowerCLI (and the `Invoke-VmScript` cmdl
##### Inputs section
I'll kick this off by going into Cloud Assembly and editing the `WindowsDemo` template I've been working on for the past few eons. I'll add a `diskSize` input:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
formatVersion: 1
inputs:
site: [...]
image: [...]
size: [...]
diskSize:
diskSize: # [tl! focus:5]
title: 'System drive size'
default: 60
type: integer
@ -49,11 +50,12 @@ inputs:
The default value is set to 60GB to match the VMDK attached to the source template; that's also the minimum value since shrinking disks gets messy.
I'll also drop in an `adminsList` input at the bottom of the section:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
[...]
poc_email: [...]
ticket: [...]
adminsList:
adminsList: # [tl! focus:4]
type: string
title: Administrators
description: Comma-separated list of domain accounts/groups which need admin access to this server.
@ -71,7 +73,8 @@ In the Resources section of the cloud template, I'm going to add a few propertie
I'll also include the `adminsList` input from earlier so that can get passed to ABX as well. And I'm going to add in an `adJoin` property (mapped to the [existing `input.adJoin`](/joining-vms-to-active-directory-in-site-specific-ous-with-vra8#cloud-template)) so that I'll have that to work with later.
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
[...]
resources:
Cloud_vSphere_Machine_1:
@ -80,7 +83,7 @@ resources:
image: '${input.image}'
flavor: '${input.size}'
site: '${input.site}'
vCenter: vcsa.lab.bowdre.net
vCenter: vcsa.lab.bowdre.net # [tl! focus:3]
vCenterUser: vra@lab.bowdre.net
templateUser: '${input.adJoin ? "vra@lab" : "Administrator"}'
adminsList: '${input.adminsList}'
@ -93,12 +96,13 @@ resources:
```
And I will add in a `storage` property as well which will automatically adjust the deployed VMDK size to match the specified input:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
[...]
description: '${input.description}'
networks: [...]
constraints: [...]
storage:
storage: # [tl! focus:1]
bootDiskCapacityInGB: '${input.diskSize}'
Cloud_vSphere_Network_1:
type: Cloud.vSphere.Network
@ -108,7 +112,8 @@ And I will add in a `storage` property as well which will automatically adjust t
##### Complete template
Okay, all together now:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
formatVersion: 1
inputs:
site:
@ -196,7 +201,7 @@ inputs:
poc_email:
type: string
title: Point of Contact Email
default: jack.shephard@virtuallypotato.com
default: jack.shephard@example.com
pattern: '^[^\s@]+@[^\s@]+\.[^\s@]+$'
ticket:
type: string
@ -296,7 +301,8 @@ And I'll pop over to the right side to map the Action Constants I created earlie
![Mapping constants in action](20210901_map_constants_to_action.png)
Now for The Script:
```powershell {linenos=true}
```powershell
# torchlight! {"lineNumbers": true}
<# vRA 8.x ABX action to perform certain in-guest actions post-deploy:
Windows:
- auto-update VM tools

View file

@ -90,7 +90,8 @@ Next it updates the links for any thumbnail images mentioned in the front matter
Lastly, it changes the `usePageBundles` flag from `false` to `true` so that Hugo knows what we've done.
```bash {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash
# Hasty script to convert a given standard Hugo post (where the post content and
# images are stored separately) to a Page Bundle (where the content and images are

View file

@ -17,14 +17,13 @@ I'm preparing to migrate this blog thingy from Hashnode (which has been great!)
Hashnode helpfully automatically backs up my posts in Markdown format to a private GitHub repo so it was easy to clone those into a local working directory, but all the embedded images were still hosted on Hashnode:
```markdown
![Clever image title](https://cdn.hashnode.com/res/hashnode/image/upload/v1600098180227/lhTnVwCO3.png)
```
I wanted to download those images to `./assets/images/posts-2020/` within my local Jekyll working directory, and then update the `*.md` files to reflect the correct local path... without doing it all manually. It took a bit of trial and error to get the regex working just right (and the result is neither pretty nor elegant), but here's what I came up with:
```bash {linenos=true}
```shell
# torchlight! {"lineNumbers": true}
#!/bin/bash
# Hasty script to process a blog post markdown file, capture the URL for embedded images,
# download the image locally, and modify the markdown file with the relative image path.
@ -49,16 +48,14 @@ done
I could then run that against all of the Markdown posts under `./_posts/` with:
```command
for post in $(ls _posts/); do ~/scripts/imageMigration.sh $post; done
```shell
for post in $(ls _posts/); do ~/scripts/imageMigration.sh $post; done # [tl! .cmd]
```
And the image embeds in the local copy of my posts now all look like this:
```markdown
![Clever image title](lhTnVwCO3.png)
```
Brilliant!

View file

@ -54,8 +54,8 @@ The first step in getting up and running with Tailscale is to sign up at [https:
Once you have a Tailscale account, you're ready to install the Tailscale client. The [download page](https://tailscale.com/download) outlines how to install it on various platforms, and also provides a handy-dandy one-liner to install it on Linux:
```command
curl -fsSL https://tailscale.com/install.sh | sh
```shell
curl -fsSL https://tailscale.com/install.sh | sh # [tl! .cmd]
```
After the install completes, it will tell you exactly what you need to do next:
@ -71,9 +71,9 @@ There are also Tailscale apps available for [iOS](https://tailscale.com/download
#### Basic `tailscale up`
Running `sudo tailscale up` then reveals the next step:
```command-session
sudo tailscale up
```shell
sudo tailscale up # [tl! .cmd]
# [tl! .nocopy:3]
To authenticate, visit:
https://login.tailscale.com/a/1872939939df
@ -83,8 +83,8 @@ I can copy that address into a browser and I'll get prompted to log in to my Tai
That was pretty easy, right? But what about if I can't easily get to a web browser from the terminal session on a certain device? No worries, `tailscale up` has a flag for that:
```command
sudo tailscale up --qr
```shell
sudo tailscale up --qr # [tl! .cmd]
```
That will convert the URL to a QR code that I can scan from my phone.
@ -93,44 +93,44 @@ That will convert the URL to a QR code that I can scan from my phone.
There are a few additional flags that can be useful under certain situations:
- `--advertise-exit-node` to tell the tailnet that this could be used as an exit node for internet traffic
```command
sudo tailscale up --advertise-exit-node
```shell
sudo tailscale up --advertise-exit-node # [tl! .cmd]
```
- `--advertise-routes` to let the node perform subnet routing functions to provide connectivity to specified local subnets
```command
sudo tailscale up --advertise-routes "192.168.1.0/24,172.16.0.0/16"
```shell
sudo tailscale up --advertise-routes "192.168.1.0/24,172.16.0.0/16" # [tl! .cmd]
```
- `--advertise-tags`[^tags] to associate the node with certain tags for ACL purposes (like `tag:home` to identify stuff in my home network and `tag:cloud` to label external cloud-hosted resources)
```command
sudo tailscale up --advertise-tags "tag:cloud"
```shell
sudo tailscale up --advertise-tags "tag:cloud" # [tl! .cmd]
```
- `--hostname` to manually specific a hostname to use within the tailnet
```command
sudo tailscale up --hostname "tailnode"
```shell
sudo tailscale up --hostname "tailnode" # [tl! .cmd]
```
- `--shields-up` to block incoming traffic
```command
sudo tailscale up --shields-up
```shell
sudo tailscale up --shields-up # [tl! .cmd]
```
These flags can also be combined with each other:
```command
sudo tailscale up --hostname "tailnode" --advertise-exit-node --qr
```shell
sudo tailscale up --hostname "tailnode" --advertise-exit-node --qr # [tl! .cmd]
```
[^tags]: Before being able to assign tags at the command line, you must first define tag owners who can manage the tag. On a personal account, you've only got one user to worry with but you still have to set this up first. I'll go over this in a bit but here's [the documentation](https://tailscale.com/kb/1068/acl-tags/#defining-a-tag) if you want to skip ahead.
#### Sidebar: Tailscale on VyOS
Getting Tailscale on [my VyOS virtual router](/vmware-home-lab-on-intel-nuc-9/#vyos) was unfortunately a little more involved than [leveraging the built-in WireGuard capability](/cloud-based-wireguard-vpn-remote-homelab-access/#configure-vyos-router-as-wireguard-peer). I found the [vyos-tailscale](https://github.com/DMarby/vyos-tailscale) project to help with building a customized VyOS installation ISO with the `tailscaled` daemon added in. I was then able to copy the ISO over to my VyOS instance and install it as if it were a [standard upgrade](https://docs.vyos.io/en/latest/installation/update.html). I could then bring up the interface, advertise my home networks, and make it available as an exit node with:
```command
sudo tailscale up --advertise-exit-node --advertise-routes "192.168.1.0/24,172.16.0.0/16"
```shell
sudo tailscale up --advertise-exit-node --advertise-routes "192.168.1.0/24,172.16.0.0/16" # [tl! .cmd]
```
#### Other `tailscale` commands
Once there are a few members, I can use the `tailscale status` command to see a quick overview of the tailnet:
```command-session
tailscale status
100.115.115.39 deb01 john@ linux -
```shell
tailscale status # [tl! .cmd]
100.115.115.39 deb01 john@ linux - # [tl! .nocopy:start]
100.118.115.69 ipam john@ linux -
100.116.90.109 johns-iphone john@ iOS -
100.116.31.85 matrix john@ linux -
@ -138,16 +138,16 @@ tailscale status
100.94.127.1 pixelbook john@ android -
100.75.110.50 snikket john@ linux -
100.96.24.81 vyos john@ linux -
100.124.116.125 win01 john@ windows -
100.124.116.125 win01 john@ windows - # [tl! .nocopy:end]
```
Without doing any other configuration beyond just installing Tailscale and connecting it to my account, I can now easily connect from any of these devices to any of the other devices using the listed Tailscale IP[^magicdns]. Entering `ssh 100.116.31.85` will connect me to my Matrix server.
`tailscale ping` lets me check the latency between two Tailscale nodes at the Tailscale layer; the first couple of pings will likely be delivered through a nearby DERP server until the NAT traversal magic is able to kick in:
```command-session
tailscale ping snikket
pong from snikket (100.75.110.50) via DERP(nyc) in 34ms
```shell
tailscale ping snikket # [tl! .cmd]
pong from snikket (100.75.110.50) via DERP(nyc) in 34ms # [tl! .nocopy:3]
pong from snikket (100.75.110.50) via DERP(nyc) in 35ms
pong from snikket (100.75.110.50) via DERP(nyc) in 35ms
pong from snikket (100.75.110.50) via [PUBLIC_IP]:41641 in 23ms
@ -155,9 +155,9 @@ pong from snikket (100.75.110.50) via [PUBLIC_IP]:41641 in 23ms
The `tailscale netcheck` command will give me some details about my local Tailscale node, like whether it's able to pass UDP traffic, which DERP server is the closest, and the latency to all Tailscale DERP servers:
```command-session
tailscale netcheck
```shell
tailscale netcheck # [tl! .cmd]
# [tl! .nocopy:start]
Report:
* UDP: true
* IPv4: yes, [LOCAL_PUBLIC_IP]:52661
@ -178,7 +178,7 @@ Report:
- tok: 154.9ms (Tokyo)
- syd: 215.3ms (Sydney)
- sin: 243.7ms (Singapore)
- blr: 244.6ms (Bangalore)
- blr: 244.6ms (Bangalore) # [tl! .nocopy:end]
```
[^magicdns]: I could also connect using the Tailscale hostname, if [MagicDNS](https://tailscale.com/kb/1081/magicdns/) is enabled - but I'm getting ahead of myself.
@ -244,7 +244,8 @@ This ACL file uses a format called [HuJSON](https://github.com/tailscale/hujson)
I'm going to start by creating a group called `admins` and add myself to that group. This isn't strictly necessary since I am the only user in the organization, but I feel like it's a nice practice anyway. Then I'll add the `tagOwners` section to map each tag to its owner, the new group I just created:
```json {linenos=true}
```json
// torchlight! {"lineNumbers": true}
{
"groups": {
"group:admins": ["john@example.com"],
@ -276,7 +277,8 @@ Each ACL rule consists of four named parts:
4. `ports` - a list of destinations (and optional ports).
So I'll add this to the top of my policy file:
```json {linenos=true}
```json
// torchlight! {"lineNumbers": true}
{
"acls": [
{
@ -305,7 +307,8 @@ Earlier I configured Tailscale to force all nodes to use my home DNS server for
2. Add a new ACL rule to allow DNS traffic to reach the DNS server from the cloud.
Option 2 sounds better to me so that's what I'm going to do. Instead of putting an IP address directly into the ACL rule I'd rather use a hostname, and unfortunately the Tailscale host names aren't available within ACL rule declarations. But I can define a host alias in the policy to map a friendly name to the IP:
```json {linenos=true}
```json
// torchlight! {"lineNumbers": true}
{
"hosts": {
"win01": "100.124.116.125"
@ -314,7 +317,8 @@ Option 2 sounds better to me so that's what I'm going to do. Instead of putting
```
And I can then create a new rule for `"users": ["tag:cloud"]` to add an exception for `win01:53`:
```json {linenos=true}
```json
// torchlight! {"lineNumbers": true}
{
"acls": [
{
@ -331,7 +335,8 @@ And I can then create a new rule for `"users": ["tag:cloud"]` to add an exceptio
And that gets DNS working again for my cloud servers while still serving the results from my NextDNS configuration. Here's the complete policy configuration:
```json {linenos=true}
```json
// torchlight! {"lineNumbers": true}
{
"acls": [
{

View file

@ -37,40 +37,41 @@ You're ready to roll once the Terminal opens and gives you a prompt:
![Hello, Penguin!](0-h1flLZs.png)
Your first action should be to go ahead and install any patches:
```command
sudo apt update
```shell
sudo apt update # [tl! .cmd:1]
sudo apt upgrade
```
### Zsh, Oh My Zsh, and powerlevel10k theme
I've been really getting into this shell setup recently so let's go on and make things comfortable before we move on too much further. Getting `zsh` is straight forward:
```command
sudo apt install zsh
```shell
sudo apt install zsh # [tl! .cmd]
```
Go ahead and launch `zsh` (by typing '`zsh`') and go through the initial setup wizard to configure preferences for things like history, completion, and other settings. I leave history on the defaults, enable the default completion options, switch the command-line editor to `vi`-style, and enable both `autocd` and `appendhistory`. Once you're back at the (new) `penguin%` prompt we can move on to installing the [Oh My Zsh plugin framework](https://github.com/ohmyzsh/ohmyzsh).
Just grab the installer script like so:
```command
wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh
```shell
wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh # [tl! .cmd]
```
Review it if you'd like (and you should! *Always* review code before running it!!), and then execute it:
```command
sh install.sh
```shell
sh install.sh # [tl! .cmd]
```
When asked if you'd like to change your default shell to `zsh` now, **say no**. This is because it will prompt for your password, but you probably don't have a password set on your brand-new Linux (Beta) account and that just makes things complicated. We'll clear this up later, but for now just check out that slick new prompt:
![Oh my!](8q-WT0AyC.png)
Oh My Zsh is pretty handy because you can easily enable [additional plugins](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins) to make your prompt behave exactly the way you want it to. Let's spruce it up even more with the [powerlevel10k theme](https://github.com/romkatv/powerlevel10k)!
```command
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k
```shell
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git \ # [tl! .cmd]
${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k
```
Now we just need to edit `~/.zshrc` to point to the new theme:
```command
sed -i s/^ZSH_THEME=.\*$/ZSH_THEME='"powerlevel10k\/powerlevel10k"'/ ~/.zshrc
```shell
sed -i s/^ZSH_THEME=.\*$/ZSH_THEME='"powerlevel10k\/powerlevel10k"'/ ~/.zshrc # [tl! .cmd]
```
We'll need to launch another instance of `zsh` for the theme change to take effect so first lets go ahead and manually set `zsh` as our default shell. We can use `sudo` to get around the whole "don't have a password set" inconvenience:
```command
sudo chsh -s /bin/zsh [username]
```shell
sudo chsh -s /bin/zsh [username] # [tl! .cmd]
```
Now close out the terminal and open it again, and you should be met by the powerlevel10k configurator which will walk you through getting things set up:
![powerlevel10k configurator](K1ScSuWcg.png)
@ -82,8 +83,8 @@ Looking good!
### Visual Studio Code
I'll need to do some light development work so VS Code is next on the hit list. You can grab the installer [here](https://code.visualstudio.com/Download#) or just copy/paste the following to stay in the Terminal. Definitely be sure to get the arm64 version!
```command
curl -L https://aka.ms/linux-arm64-deb > code_arm64.deb
```shell
curl -L https://aka.ms/linux-arm64-deb > code_arm64.deb # [tl! .cmd:1]
sudo apt install ./code_arm64.deb
```
VS Code should automatically appear in the Chromebook's Launcher, or you can use it to open a file directly with `code [filename]`:
@ -104,8 +105,8 @@ Once you connect the phone to Linux, check the phone to approve the debugging co
I'm working on setting up a [VMware homelab on an Intel NUC 9](https://twitter.com/johndotbowdre/status/1317558182936563714) so being able to automate things with PowerCLI will be handy.
PowerShell for ARM is still in an early stage so while [it is supported](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7.2#support-for-arm-processors) it must be installed manually. Microsoft has instructions for installing PowerShell from binary archives [here](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7.2#linux), and I grabbed the latest `-linux-arm64.tar.gz` release I could find [here](https://github.com/PowerShell/PowerShell/releases).
```command
curl -L -o /tmp/powershell.tar.gz https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/powershell-7.2.0-preview.5-linux-arm64.tar.gz
```shell
curl -L -o /tmp/powershell.tar.gz https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/powershell-7.2.0-preview.5-linux-arm64.tar.gz # [tl! .cmd:4]
sudo mkdir -p /opt/microsoft/powershell/7
sudo tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7
sudo chmod +x /opt/microsoft/powershell/7/pwsh
@ -124,8 +125,8 @@ Woot!
The Linux (Beta) environment consists of a hardened virtual machine (named `termina`) running an LXC Debian container (named `penguin`). Know what would be even more fun? Let's run some other containers inside our container!
The docker installation has a few prerequisites:
```command-session
sudo apt install \
```shell
sudo apt install \ # [tl! .cmd]
apt-transport-https \
ca-certificates \
curl \
@ -133,19 +134,19 @@ sudo apt install \
software-properties-common
```
Then we need to grab the Docker repo key:
```command
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
```shell
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add - # [tl! .cmd]
```
And then we can add the repo:
```command-session
sudo add-apt-repository \
```shell
sudo add-apt-repository \ # [tl! .cmd]
"deb [arch=arm64] https://download.docker.com/linux/debian \
$(lsb_release -cs) \
stable"
```
And finally update the package cache and install `docker` and its friends:
```command
sudo apt update
```shell
sudo apt update # [tl! .cmd:1]
sudo apt install docker-ce docker-ce-cli containerd.io
```
![I put a container in your container](k2uiYi5e8.png)
@ -163,14 +164,14 @@ So while I can use the Duet for designing 3D models, I won't be able to actually
I came across [a Reddit post](https://www.reddit.com/r/Crostini/comments/jnbqv3/successfully_running_jupyter_notebook_on_samsung/) today describing how to install `conda` and get a Jupyter Notebook running on arm64 so I had to give it a try. It actually wasn't that bad!
The key is to grab the appropriate version of [conda Miniforge](https://github.com/conda-forge/miniforge), make it executable, and run the installer:
```command
wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-aarch64.sh
```shell
wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-aarch64.sh # [tl! .cmd:2]
chmod +x Miniforge3-Linux-aarch64.sh
./Miniforge3-Linux-aarch64.sh
```
Exit the terminal and relaunch it, and then install Jupyter:
```command
conda install -c conda-forge notebook
```shell
conda install -c conda-forge notebook # [tl! .cmd]
```
You can then launch the notebook with `jupyter notebook` and it will automatically open up in a Chrome OS browser tab:

View file

@ -58,8 +58,8 @@ You can refer to my notes from last time for details on how I [created the Ubunt
| `60000-60100`[^4] | UDP | Audio/Video data proxy (TURN data) |
As a gentle reminder, Oracle's `iptables` configuration inserts a `REJECT all` rule at the bottom of each chain. I needed to make sure that each of my `ALLOW` rules get inserted above that point. So I used `iptables -L INPUT --line-numbers` to identify which line held the `REJECT` rule, and then used `iptables -I INPUT [LINE_NUMBER] -m state --state NEW -p [PROTOCOL] --dport [PORT] -j ACCEPT` to insert the new rules above that point.
```command
sudo iptables -I INPUT 9 -m state --state NEW -p tcp --dport 80 -j ACCEPT
```shell
sudo iptables -I INPUT 9 -m state --state NEW -p tcp --dport 80 -j ACCEPT # [tl! .cmd:start]
sudo iptables -I INPUT 9 -m state --state NEW -p tcp --dport 443 -j ACCEPT
sudo iptables -I INPUT 9 -m state --state NEW -p tcp --dports 3478-3479 -j ACCEPT
sudo iptables -I INPUT 9 -m state --state NEW -p tcp -m multiport --dports 3478-3479 -j ACCEPT
@ -69,13 +69,13 @@ sudo iptables -I INPUT 9 -m state --state NEW -p tcp --dport 5222 -j ACCEPT
sudo iptables -I INPUT 9 -m state --state NEW -p tcp --dport 5269 -j ACCEPT
sudo iptables -I INPUT 9 -m state --state NEW -p udp -m multiport --dports 3478,3479 -j ACCEPT
sudo iptables -I INPUT 9 -m state --state NEW -p udp -m multiport --dports 5349,5350 -j ACCEPT
sudo iptables -I INPUT 9 -m state --state NEW -p udp -m multiport --dports 60000:60100 -j ACCEPT
sudo iptables -I INPUT 9 -m state --state NEW -p udp -m multiport --dports 60000:60100 -j ACCEPT # [tl! .cmd:end]
```
Then to verify the rules are in the right order:
```command-session
sudo iptables -L INPUT --line-numbers -n
Chain INPUT (policy ACCEPT)
```shell
sudo iptables -L INPUT --line-numbers -n # [tl! .cmd]
Chain INPUT (policy ACCEPT) # [tl! .nocopy:start]
num target prot opt source destination
1 ts-input all -- 0.0.0.0/0 0.0.0.0/0
2 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
@ -92,13 +92,13 @@ num target prot opt source destination
13 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:5222
14 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:5000
15 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW multiport dports 3478,3479
16 REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
16 REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited # [tl! .nocopy:end]
```
Before moving on, it's important to save them so the rules will persist across reboots!
```command-session
sudo netfilter-persistent save
run-parts: executing /usr/share/netfilter-persistent/plugins.d/15-ip4tables save
```shell
sudo netfilter-persistent save # [tl! .cmd]
run-parts: executing /usr/share/netfilter-persistent/plugins.d/15-ip4tables save # [tl! .nocopy:1]
run-parts: executing /usr/share/netfilter-persistent/plugins.d/25-ip6tables save
```
@ -115,30 +115,30 @@ share.vpota.to 300 IN CNAME chat.vpota.to
### Install `docker` and `docker-compose`
Snikket is distributed as a set of docker containers which makes it super easy to get up and running on basically any Linux system. But, of course, you'll first need to [install `docker`](https://docs.docker.com/engine/install/ubuntu/)
```bash
```shell
# Update package index
sudo apt update
sudo apt update # [tl! .cmd]
# Install prereqs
sudo apt install ca-certificates curl gnupg lsb-release
sudo apt install ca-certificates curl gnupg lsb-release # [tl! .cmd]
# Add docker's GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # [tl! .cmd]
# Add the docker repo
echo \
echo \ # [tl! .cmd]
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Refresh the package index with the new repo added
sudo apt update
sudo apt update # [tl! .cmd]
# Install docker
sudo apt install docker-ce docker-ce-cli containerd.io
sudo apt install docker-ce docker-ce-cli containerd.io # [tl! .cmd]
```
And install `docker-compose` also to simplify the container management:
```bash
```shell
# Download the docker-compose binary
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose # [tl! .cmd]
# Make it executable
sudo chmod +x /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose # [tl! .cmd]
```
Now we're ready to...
@ -146,21 +146,21 @@ Now we're ready to...
### Install Snikket
This starts with just making a place for Snikket to live:
```command
sudo mkdir /etc/snikket
```shell
sudo mkdir /etc/snikket # [tl! .cmd:1]
cd /etc/snikket
```
And then grabbing the Snikket `docker-compose` file:
```command
sudo curl -o docker-compose.yml https://snikket.org/service/resources/docker-compose.beta.yml
```shell
sudo curl -o docker-compose.yml https://snikket.org/service/resources/docker-compose.beta.yml # [tl! .cmd]
```
And then creating a very minimal configuration file:
```command
sudo vi snikket.conf
```shell
sudo vi snikket.conf # [tl! .cmd]
```
A basic config only needs two parameters:
@ -176,7 +176,8 @@ In my case, I'm going to add two additional parameters to restrict the UDP TURN
So here's my config:
```cfg {linenos=true}
```ini
# torchlight! {"lineNumbers": true}
SNIKKET_DOMAIN=chat.vpota.to
SNIKKET_ADMIN_EMAIL=ops@example.com
@ -188,8 +189,8 @@ SNIKKET_TWEAK_TURNSERVER_MAX_PORT=60100
### Start it up!
With everything in place, I can start up the Snikket server:
```command
sudo docker-compose up -d
```shell
sudo docker-compose up -d # [tl! .cmd]
```
This will take a moment or two to pull down all the required container images, start them, and automatically generate the SSL certificates. Very soon, though, I can point my browser to `https://chat.vpota.to` and see a lovely login page - complete with an automagically-valid-and-trusted certificate:
@ -197,8 +198,8 @@ This will take a moment or two to pull down all the required container images, s
Of course, I don't yet have a way to log in, and like I mentioned earlier Snikket doesn't offer open user registration. Every user (even me, the admin!) has to be invited. Fortunately I can generate my first invite directly from the command line:
```command
sudo docker exec snikket create-invite --admin --group default
```shell
sudo docker exec snikket create-invite --admin --group default # [tl! .cmd]
```
That command will return a customized invite link which I can copy and paste into my browser.
@ -251,8 +252,9 @@ One of the really cool things about Caddy is that it automatically generates SSL
Fortunately, the [Snikket reverse proxy documentation](https://github.com/snikket-im/snikket-server/blob/master/docs/advanced/reverse_proxy.md#basic) was recently updated with a sample config for making this happen. Matrix and Snikket really only overlap on ports `80` and `443` so those are the only ports I'll need to handle, which lets me go for the "Basic" configuration instead of the "Advanced" one. I can just adapt the sample config from the documentation and add that to my existing `/etc/caddy/Caddyfile` alongside the config for Matrix:
```caddy {linenos=true}
http://chat.vpota.to,
```text
# torchlight! {"lineNumbers": true}
http://chat.vpota.to, # [tl! focus:start]
http://groups.chat.vpota.to,
http://share.chat.vpota.to {
reverse_proxy localhost:5080
@ -266,7 +268,7 @@ share.chat.vpota.to {
tls_insecure_skip_verify
}
}
}
} # [tl! focus:end]
matrix.bowdre.net {
reverse_proxy /_matrix/* http://localhost:8008
@ -294,34 +296,32 @@ Since Snikket is completely containerized, moving between hosts is a simple matt
The Snikket team has actually put together a couple of scripts to assist with [backing up](https://github.com/snikket-im/snikket-selfhosted/blob/main/scripts/backup.sh) and [restoring](https://github.com/snikket-im/snikket-selfhosted/blob/main/scripts/restore.sh) an instance. I just adapted the last line of each to do what I needed:
```command-session
sudo docker run --rm --volumes-from=snikket \
```shell
sudo docker run --rm --volumes-from=snikket \ # [tl! .cmd]
-v "/home/john/snikket-backup/":/backup debian:buster-slim \
tar czf /backup/snikket-"$(date +%F-%H%m)".tar.gz /snikket
```
That will drop a compressed backup of the `snikket_data` volume into the specified directory, `/home/john/snikket-backup/`. While I'm at it, I'll also go ahead and copy the `docker-compose.yml` and `snikket.conf` files from `/etc/snikket/`:
```command
sudo cp -a /etc/snikket/* /home/john/snikket-backup/
```
```command-session
ls -l /home/john/snikket-backup/
total 1728
```shell
sudo cp -a /etc/snikket/* /home/john/snikket-backup/ # [tl! .cmd]
ls -l /home/john/snikket-backup/ # [tl! .cmd]
total 1728 # [tl! .nocopy:3]
-rw-r--r-- 1 root root 993 Dec 19 17:47 docker-compose.yml
-rw-r--r-- 1 root root 1761046 Dec 19 17:46 snikket-2021-12-19-1745.tar.gz
-rw-r--r-- 1 root root 299 Dec 19 17:47 snikket.conf
```
And I can then zip that up for easy transfer:
```command
tar cvf /home/john/snikket-backup.tar.gz /home/john/snikket-backup/
```shell
tar cvf /home/john/snikket-backup.tar.gz /home/john/snikket-backup/ # [tl! .cmd]
```
This would be a great time to go ahead and stop this original Snikket instance. After all, nothing that happens after the backup was exported is going to carry over anyway.
```command
sudo docker-compose down
```shell
sudo docker-compose down # [tl! .cmd]
```
{{% notice tip "Update DNS" %}}
This is also a great time to update the `A` record for `chat.vpota.to` so that it points to the new server. It will need a little bit of time for the change to trickle out, and the updated record really needs to be in place before starting Snikket on the new server so that there aren't any certificate problems.
@ -330,20 +330,18 @@ This is also a great time to update the `A` record for `chat.vpota.to` so that i
Now I just need to transfer the archive from one server to the other. I've got [Tailscale](https://tailscale.com/)[^11] running on my various cloud servers so that they can talk to each other through a secure WireGuard tunnel (remember [WireGuard](/cloud-based-wireguard-vpn-remote-homelab-access/)?) without having to open any firewall ports between them, and that means I can just use `scp` to transfer the file without any fuss. I can even leverage Tailscale's [Magic DNS](https://tailscale.com/kb/1081/magicdns/) feature to avoid worrying with any IPs, just the hostname registered in Tailscale (`chat-oci`):
```command
scp /home/john/snikket-backup.tar.gz chat-oci:/home/john/
```shell
scp /home/john/snikket-backup.tar.gz chat-oci:/home/john/ # [tl! .cmd]
```
Next, I SSH in to the new server and unzip the archive:
```command
ssh snikket-oci-server
```shell
ssh snikket-oci-server # [tl! .cmd:3]
tar xf snikket-backup.tar.gz
cd snikket-backup
```
```command-session
ls -l
total 1728
total 1728 # [tl! .nocopy:3]
-rw-r--r-- 1 root root 993 Dec 19 17:47 docker-compose.yml
-rw-r--r-- 1 root root 1761046 Dec 19 17:46 snikket-2021-12-19-1745.tar.gz
-rw-r--r-- 1 root root 299 Dec 19 17:47 snikket.conf
@ -351,8 +349,8 @@ total 1728
Before I can restore the content of the `snikket-data` volume on the new server, I'll need to first go ahead and set up Snikket again. I've already got `docker` and `docker-compose` installed from when I installed Matrix so I'll skip to creating the Snikket directory and copying in the `docker-compose.yml` and `snikket.conf` files.
```command
sudo mkdir /etc/snikket
```shell
sudo mkdir /etc/snikket # [tl! .cmd:3]
sudo cp docker-compose.yml /etc/snikket/
sudo cp snikket.conf /etc/snikket/
cd /etc/snikket
@ -360,7 +358,8 @@ cd /etc/snikket
Before I fire this up on the new host, I need to edit the `snikket.conf` to tell Snikket to use those different ports defined in the reverse proxy configuration using [a couple of `SNIKKET_TWEAK_*` lines](https://github.com/snikket-im/snikket-server/blob/master/docs/advanced/reverse_proxy.md#snikket):
```cfg {linenos=true}
```ini
# torchlight! {"lineNumbers": true}
SNIKKET_DOMAIN=chat.vpota.to
SNIKKET_ADMIN_EMAIL=ops@example.com
@ -371,16 +370,16 @@ SNIKKET_TWEAK_TURNSERVER_MAX_PORT=60100
```
Alright, let's start up the Snikket server:
```command
sudo docker-compose up -d
```shell
sudo docker-compose up -d # [tl! .cmd]
```
After a moment or two, I can point a browser to `https://chat.vpota.to` and see the login screen (with a valid SSL certificate!) but I won't actually be able to log in. As far as Snikket is concerned, this is a brand new setup.
Now I can borrow the last line from the [`restore.sh` script](https://github.com/snikket-im/snikket-selfhosted/blob/main/scripts/restore.sh) to bring in my data:
```command-session
sudo docker run --rm --volumes-from=snikket \
```shell
sudo docker run --rm --volumes-from=snikket \ # [tl! .cmd]
--mount type=bind,source="/home/john/snikket-backup/snikket-2021-12-19-1745.tar.gz",destination=/backup.tar.gz \
debian:buster-slim \
bash -c "rm -rf /snikket/*; tar xvf /backup.tar.gz -C /"

View file

@ -15,8 +15,8 @@ tags:
Following a recent update, I found that the [Linux development environment](https://chromium.googlesource.com/chromiumos/docs/+/HEAD/containers_and_vms.md) on my Framework Chromebook would fail to load if the [Tailscale](/secure-networking-made-simple-with-tailscale) daemon was already running. It seems that the Tailscale virtual interface may have interfered with how the CrOS Terminal app was expecting to connect to the Linux container. I initially worked around the problem by just disabling the `tailscaled` service, but having to remember to start it up manually was a pretty heavy cognitive load.
Fortunately, it turns out that overriding the service to insert a short startup delay is really easy. I'll just use the `systemctl edit` command to create a quick override configuration:
```command
sudo systemctl edit tailscaled
```shell
sudo systemctl edit tailscaled # [tl! .cmd]
```
This shows me the existing contents of the `tailscaled.service` definition so I can easily insert some overrides above. In this case, I just want to use `sleep 5` as the `ExecStartPre` command so that the service start will be delayed by 5 seconds:

View file

@ -36,7 +36,7 @@ Sounds great - but how do you actually make golink available on your tailnet? We
There are three things I'll need to do in the Tailscale admin portal before moving on:
#### Create an ACL tag
I assign ACL tags to devices in my tailnet based on their location and/or purpose, and I'm then able to use those in a policy to restrict access between certain devices. To that end, I'm going to create a new `tag:golink` tag for this purpose. Creating a new tag in Tailscale is really just going to the [Access Controls page of the admin console](https://login.tailscale.com/admin/acls) and editing the policy to specify a `tagOwner` who is permitted to assign the tag:
```text {hl_lines=[11]}
```json
"groups":
"group:admins": ["john@example.com"],
},
@ -47,14 +47,14 @@ I assign ACL tags to devices in my tailnet based on their location and/or purpos
"tag:dns": ["group:admins"],
"tag:rsync": ["group:admins"],
"tag:funnel": ["group:admins"],
"tag:golink": ["group:admins"],
"tag:golink": ["group:admins"], // [tl! highlight]
},
```
#### Configure ACL access
This step is really only necessary since I've altered the default Tailscale ACL and prevent my nodes from communicating with each other unless specifically permitted. I want to make sure that everything on my tailnet can access golink:
```text
```json
"acls": [
{
// make golink accessible to everything
@ -80,20 +80,21 @@ After clicking the **Generate key** button, the key will be displayed. This is t
### Docker setup
The [golink repo](https://github.com/tailscale/golink) offers this command for running the container:
```command
docker run -it --rm ghcr.io/tailscale/golink:main
```shell
docker run -it --rm ghcr.io/tailscale/golink:main # [tl! .cmd]
```
The doc also indicates that I can pass the auth key to the golink service via the `TS_AUTHKEY` environment variable, and that all the configuration will be stored in `/home/nonroot` (which will be owned by uid/gid `65532`). I'll take this knowledge and use it to craft a `docker-compose.yml` to simplify container management.
```command
mkdir -p golink/data
```shell
mkdir -p golink/data # [tl! .cmd:3]
cd golink
chmod 65532:65532 data
vi docker-compose.yaml
```
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
# golink docker-compose.yaml
version: '3'
services:
@ -146,8 +147,8 @@ Some of my other golinks:
You can browse to `go/.export` to see a JSON-formatted listing of all configured shortcuts - or, if you're clever, you could do something like `curl http://go/.export -o links.json` to download a copy.
To restore, just pass `--snapshot /path/to/links.json` when starting golink. What I usually do is copy the file into the `data` folder that I'm mounting as a Docker volume, and then just run:
```command
sudo docker exec golink /golink --sqlitedb /home/nonroot/golink.db --snapshot /home/nonroot/links.json
```shell
sudo docker exec golink /golink --sqlitedb /home/nonroot/golink.db --snapshot /home/nonroot/links.json # [tl! .cmd]
```
### Conclusion

View file

@ -30,21 +30,21 @@ Here's a condensed list of the [steps that I took to manually install Tailscale]
1. Visit [https://pkgs.tailscale.com/stable/#static](https://pkgs.tailscale.com/stable/#static) to see the latest stable version for your system architecture, and copy the URL. For instance, I'll be using `https://pkgs.tailscale.com/stable/tailscale_1.34.1_arm64.tgz`.
2. Download and extract it to the system:
```command
wget https://pkgs.tailscale.com/stable/tailscale_1.34.1_arm64.tgz
```shell
wget https://pkgs.tailscale.com/stable/tailscale_1.34.1_arm64.tgz # [tl! .cmd:2]
tar xvf tailscale_1.34.1_arm64.tgz
cd tailscale_1.34.1_arm64/
```
3. Install the binaries and service files:
```command
sudo install -m 755 tailscale /usr/bin/
```shell
sudo install -m 755 tailscale /usr/bin/ # [tl! .cmd:4]
sudo install -m 755 tailscaled /usr/sbin/
sudo install -m 644 systemd/tailscaled.defaults /etc/default/tailscaled
sudo install -m 644 systemd/tailscaled.service /usr/lib/systemd/system/
```
4. Start the service:
```command
sudo systemctl enable tailscaled
```shell
sudo systemctl enable tailscaled # [tl! .cmd:1]
sudo systemctl start tailscaled
```

View file

@ -68,9 +68,9 @@ I've already got Docker installed on this machine, but if I didn't I would follo
I also verify that my install is using `cgroup` version 1 as version 2 is not currently supported:
```command-session
docker info | grep -i cgroup
Cgroup Driver: cgroupfs
```shell
docker info | grep -i cgroup # [tl! .cmd]
Cgroup Driver: cgroupfs # [tl! .nocopy:1]
Cgroup Version: 1
```
@ -79,64 +79,49 @@ Next up, I'll install `kubectl` [as described here](https://kubernetes.io/docs/t
I can look at the [releases page on GithHub](https://github.com/kubernetes/kubernetes/releases) to see that the latest release for me is `1.22.5`. With this newfound knowledge I can follow the [Install kubectl binary with curl on Linux](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/#install-kubectl-binary-with-curl-on-linux) instructions to grab that specific version:
```command-session
curl -LO https://dl.k8s.io/release/v1.22.5/bin/linux/amd64/kubectl
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 154 100 154 0 0 2298 0 --:--:-- --:--:-- --:--:-- 2298
100 44.7M 100 44.7M 0 0 56.9M 0 --:--:-- --:--:-- --:--:-- 56.9M
```
```command-session
```shell
curl -sLO https://dl.k8s.io/release/v1.22.5/bin/linux/amd64/kubectl # [tl! .cmd:1]
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
# [tl! .nocopy:2]
[sudo] password for john:
```
```command-session
kubectl version --client
Client Version: version.Info{Major:"1", Minor:"22", GitVersion:"v1.22.5", GitCommit:"5c99e2ac2ff9a3c549d9ca665e7bc05a3e18f07e", GitTreeState:"clean", BuildDate:"2021-12-16T08:38:33Z", GoVersion:"go1.16.12", Compiler:"gc", Platform:"linux/amd64"}
kubectl version --client # [tl! .cmd]
Client Version: version.Info{Major:"1", Minor:"22", GitVersion:"v1.22.5", # [tl! .nocopy:3]
GitCommit:"5c99e2ac2ff9a3c549d9ca665e7bc05a3e18f07e", GitTreeState:"clean",
BuildDate:"2021-12-16T08:38:33Z", GoVersion:"go1.16.12", Compiler:"gc",
Platform:"linux/amd64"}
```
#### `kind` binary
It's not strictly a requirement, but having the `kind` executable available will be handy for troubleshooting during the bootstrap process in case anything goes sideways. It can be installed in basically the same was as `kubectl`:
```command-session
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.11.1/kind-linux-amd64
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 98 100 98 0 0 513 0 --:--:-- --:--:-- --:--:-- 513
100 655 100 655 0 0 2212 0 --:--:-- --:--:-- --:--:-- 10076
100 6660k 100 6660k 0 0 11.8M 0 --:--:-- --:--:-- --:--:-- 11.8M
```
```command
```shell
curl -sLo ./kind https://kind.sigs.k8s.io/dl/v0.11.1/kind-linux-amd64 # [tl! .cmd:2]
sudo install -o root -g root -m 0755 kind /usr/local/bin/kind
```
```command-session
kind version
kind v0.11.1 go1.16.5 linux/amd64
kind v0.11.1 go1.16.5 linux/amd64 # [tl! .nocopy]
```
#### Tanzu CLI
The final bit of required software is the Tanzu CLI, which can be downloaded from the [project on GitHub](https://github.com/vmware-tanzu/community-edition/releases).
```command-session
curl -H "Accept: application/vnd.github.v3.raw" \
```shell
curl -H "Accept: application/vnd.github.v3.raw" \ # [tl! .cmd]
-L https://api.github.com/repos/vmware-tanzu/community-edition/contents/hack/get-tce-release.sh | \
bash -s v0.9.1 linux
```
And then unpack it and run the installer:
```command
tar xf tce-linux-amd64-v0.9.1.tar.gz
```shell
tar xf tce-linux-amd64-v0.9.1.tar.gz # [tl! .cmd:2]
cd tce-linux-amd64-v0.9.1
./install.sh
```
I can then verify the installation is working correctly:
```command-session
tanzu version
version: v0.2.1
```shell
tanzu version # [tl! .cmd]
version: v0.2.1 # [tl! .nocopy:2]
buildDate: 2021-09-29
sha: ceaa474
```
@ -146,15 +131,15 @@ Okay, now it's time for the good stuff - creating some shiny new Tanzu clusters!
#### Management cluster
I need to create a Management cluster first and I'd like to do that with the UI, so that's as simple as:
```command
tanzu management-cluster create --ui
```shell
tanzu management-cluster create --ui # [tl! .cmd]
```
I should then be able to access the UI by pointing a web browser at `http://127.0.0.1:8080`... but I'm running this on a VM without a GUI, so I'll need to back up and tell it to bind on `0.0.0.0:8080` so the web installer will be accessible across the network. I can also include `--browser none` so that the installer doesn't bother with trying to launch a browser locally.
```command-session
tanzu management-cluster create --ui --bind 0.0.0.0:8080 --browser none
```shell
tanzu management-cluster create --ui --bind 0.0.0.0:8080 --browser none # [tl! .cmd]
# [tl! .nocopy:2]
Validating the pre-requisites...
Serving kickstart UI at http://[::]:8080
```
@ -190,20 +175,22 @@ I skip the Tanzu Mission Control piece (since I'm still waiting on access to [TM
See the option at the bottom to copy the CLI command? I'll need to use that since clicking the friendly **Deploy** button doesn't seem to work while connected to the web server remotely.
```command
tanzu management-cluster create --file /home/john/.config/tanzu/tkg/clusterconfigs/dr94t3m2on.yaml -v 6
```shell
tanzu management-cluster create \ # [tl! .cmd]
--file /home/john/.config/tanzu/tkg/clusterconfigs/dr94t3m2on.yaml -v 6
```
In fact, I'm going to copy that file into my working directory and give it a more descriptive name so that I can re-use it in the future.
```command
cp ~/.config/tanzu/tkg/clusterconfigs/dr94t3m2on.yaml ~/projects/tanzu-homelab/tce-mgmt.yaml
```shell
cp ~/.config/tanzu/tkg/clusterconfigs/dr94t3m2on.yaml \ # [tl! .cmd]
~/projects/tanzu-homelab/tce-mgmt.yaml
```
Now I can run the install command:
```command
tanzu management-cluster create --file ./tce-mgmt.yaml -v 6
```shell
tanzu management-cluster create --file ./tce-mgmt.yaml -v 6 # [tl! .cmd]
```
After a moment or two of verifying prerequisites, I'm met with a polite offer to enable Tanzu Kubernetes Grid Service in vSphere:
@ -250,9 +237,9 @@ Some addons might be getting installed! Check their status by running the follow
I can run that last command to go ahead and verify that the addon installation has completed:
```command-session
kubectl get apps -A
NAMESPACE NAME DESCRIPTION SINCE-DEPLOY AGE
```shell
kubectl get apps -A # [tl! .cmd]
NAMESPACE NAME DESCRIPTION SINCE-DEPLOY AGE # [tl! .nocopy:5]
tkg-system antrea Reconcile succeeded 26s 6m49s
tkg-system metrics-server Reconcile succeeded 36s 6m49s
tkg-system tanzu-addons-manager Reconcile succeeded 22s 8m54s
@ -261,9 +248,9 @@ tkg-system vsphere-csi Reconcile succeeded 36s 6m50s
```
And I can use the Tanzu CLI to get some other details about the new management cluster:
```command-session
tanzu management-cluster get tce-mgmt
NAME NAMESPACE STATUS CONTROLPLANE WORKERS KUBERNETES ROLES
```shell
tanzu management-cluster get tce-mgmt # [tl! .cmd]
NAME NAMESPACE STATUS CONTROLPLANE WORKERS KUBERNETES ROLES # [tl! .nocopy:start]
tce-mgmt tkg-system running 1/1 1/1 v1.21.2+vmware.1 management
@ -285,7 +272,7 @@ Providers:
capi-kubeadm-bootstrap-system bootstrap-kubeadm BootstrapProvider kubeadm v0.3.23
capi-kubeadm-control-plane-system control-plane-kubeadm ControlPlaneProvider kubeadm v0.3.23
capi-system cluster-api CoreProvider cluster-api v0.3.23
capv-system infrastructure-vsphere InfrastructureProvider vsphere v0.7.10
capv-system infrastructure-vsphere InfrastructureProvider vsphere v0.7.10 # [tl! .nocopy:end]
```
@ -296,8 +283,8 @@ Excellent! Things are looking good so I can move on to create the cluster which
#### Workload cluster
I won't use the UI for this but will instead take a copy of my `tce-mgmt.yaml` file and adapt it to suit the workload needs (as described [here](https://tanzucommunityedition.io/docs/latest/workload-clusters/)).
```command
cp tce-mgmt.yaml tce-work.yaml
```shell
cp tce-mgmt.yaml tce-work.yaml # [tl! .cmd:1]
vi tce-work.yaml
```
@ -314,9 +301,9 @@ I *could* change a few others if I wanted to[^i_wont]:
After saving my changes to the `tce-work.yaml` file, I'm ready to deploy the cluster:
```command-session
tanzu cluster create --file tce-work.yaml
Validating configuration...
```shell
tanzu cluster create --file tce-work.yaml # [tl! .cmd]
Validating configuration... # [tl! .nocopy:start]
Warning: Pinniped configuration not found. Skipping pinniped configuration in workload cluster. Please refer to the documentation to check if you can configure pinniped on workload cluster manually
Creating workload cluster 'tce-work'...
Waiting for cluster to be initialized...
@ -324,13 +311,13 @@ Waiting for cluster nodes to be available...
Waiting for addons installation...
Waiting for packages to be up and running...
Workload cluster 'tce-work' created
Workload cluster 'tce-work' created # [tl! .nocopy:end]
```
Right on! I'll use `tanzu cluster get` to check out the workload cluster:
```command-session
tanzu cluster get tce-work
NAME NAMESPACE STATUS CONTROLPLANE WORKERS KUBERNETES ROLES
```shell
tanzu cluster get tce-work # [tl! .cmd]
NAME NAMESPACE STATUS CONTROLPLANE WORKERS KUBERNETES ROLES # [tl! .nocopy:start]
tce-work default running 1/1 1/1 v1.21.2+vmware.1 <none>
@ -343,7 +330,7 @@ NAME READY SEVERITY RE
│ └─Machine/tce-work-control-plane-8km9m True 9m31s
└─Workers
└─MachineDeployment/tce-work-md-0
└─Machine/tce-work-md-0-687444b744-cck4x True 8m31s
└─Machine/tce-work-md-0-687444b744-cck4x True 8m31s # [tl! .nocopy:end]
```
I can also go into vCenter and take a look at the VMs which constitute the two clusters:
@ -360,9 +347,9 @@ Excellent, I've got a Tanzu management cluster and a Tanzu workload cluster. Wha
If I run `kubectl get nodes` right now, I'll only get information about the management cluster:
```command-session
kubectl get nodes
NAME STATUS ROLES AGE VERSION
```shell
kubectl get nodes # [tl! .cmd]
NAME STATUS ROLES AGE VERSION # [tl! .nocopy:2]
tce-mgmt-control-plane-xtdnx Ready control-plane,master 18h v1.21.2+vmware.1
tce-mgmt-md-0-745b858d44-4c9vv Ready <none> 17h v1.21.2+vmware.1
```
@ -370,30 +357,29 @@ tce-mgmt-md-0-745b858d44-4c9vv Ready <none> 17h v1.21.2+v
#### Setting the right context
To be able to deploy stuff to the workload cluster, I need to tell `kubectl` how to talk to it. And to do that, I'll first need to use `tanzu` to capture the cluster's kubeconfig:
```command-session
tanzu cluster kubeconfig get tce-work --admin
Credentials of cluster 'tce-work' have been saved
```shell
tanzu cluster kubeconfig get tce-work --admin # [tl! .cmd]
Credentials of cluster 'tce-work' have been saved # [tl! .nocopy:1]
You can now access the cluster by running 'kubectl config use-context tce-work-admin@tce-work'
```
I can now run `kubectl config get-contexts` and see that I have access to contexts on both management and workload clusters:
```command-session
kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
```shell
kubectl config get-contexts # [tl! .cmd]
CURRENT NAME CLUSTER AUTHINFO NAMESPACE # [tl! .nocopy:2]
* tce-mgmt-admin@tce-mgmt tce-mgmt tce-mgmt-admin
tce-work-admin@tce-work tce-work tce-work-admin
```
And I can switch to the `tce-work` cluster like so:
```command-session
kubectl config use-context tce-work-admin@tce-work
Switched to context "tce-work-admin@tce-work".
```
```command-session
kubectl get nodes
NAME STATUS ROLES AGE VERSION
```shell
kubectl config use-context tce-work-admin@tce-work # [tl! .cmd]
Switched to context "tce-work-admin@tce-work". # [tl! .nocopy]
kubectl get nodes # [tl! .cmd]
NAME STATUS ROLES AGE VERSION # [tl! .nocopy:2]
tce-work-control-plane-8km9m Ready control-plane,master 17h v1.21.2+vmware.1
tce-work-md-0-687444b744-cck4x Ready <none> 17h v1.21.2+vmware.1
```
@ -405,13 +391,12 @@ Before I move on to deploying actually *useful* workloads, I'll start with deplo
I can check out the sample deployment that William put together [here](https://github.com/lamw/vmware-k8s-app-demo/blob/master/yelb.yaml), and then deploy it with:
```command-session
kubectl create ns yelb
namespace/yelb created
```
```command-session
kubectl apply -f https://raw.githubusercontent.com/lamw/vmware-k8s-app-demo/master/yelb.yaml
service/redis-server created
```shell
kubectl create ns yelb # [tl! .cmd]
namespace/yelb created # [tl! .nocopy:1]
kubectl apply -f https://raw.githubusercontent.com/lamw/vmware-k8s-app-demo/master/yelb.yaml # [tl! .cmd]
service/redis-server created # [tl! .nocopy:start]
service/yelb-db created
service/yelb-appserver created
service/yelb-ui created
@ -419,10 +404,9 @@ deployment.apps/yelb-ui created
deployment.apps/redis-server created
deployment.apps/yelb-db created
deployment.apps/yelb-appserver created
```
```command-session
kubectl -n yelb get pods
NAME READY STATUS RESTARTS AGE
# [tl! .nocopy:end]
kubectl -n yelb get pods # [tl! .cmd]
NAME READY STATUS RESTARTS AGE # [tl! .nocopy:4]
redis-server-74556bbcb7-r9jqc 1/1 Running 0 10s
yelb-appserver-d584bb889-2jspg 1/1 Running 0 10s
yelb-db-694586cd78-wb8tt 1/1 Running 0 10s
@ -431,36 +415,35 @@ yelb-ui-8f54fd88c-k2dw9 1/1 Running 0 10s
Once the app is running, I can point my web browser at it to see it in action. But what IP do I use?
```command-session
kubectl -n yelb get svc/yelb-ui
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
```shell
kubectl -n yelb get svc/yelb-ui # [tl! .cmd]
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE # [tl! .nocopy:1]
yelb-ui NodePort 100.71.228.116 <none> 80:30001/TCP 84s
```
This demo is using a `NodePort` type service to expose the front end, which means it will be accessible on port `30001` on the node it's running on. I can find that IP by:
```command-session
kubectl -n yelb describe pod $(kubectl -n yelb get pods | grep yelb-ui | awk '{print $1}') | grep "Node:"
Node: tce-work-md-0-687444b744-cck4x/192.168.1.145
```shell
kubectl -n yelb describe pod $(kubectl -n yelb get pods | grep yelb-ui | awk '{print $1}') | grep "Node:" # [tl! .cmd]
Node: tce-work-md-0-687444b744-cck4x/192.168.1.145 # [tl! .nocopy]
```
So I can point my browser at `http://192.168.1.145:30001` and see the demo:
![yelb demo page](yelb_nodeport_demo.png)
After marveling at my own magnificence[^magnificence] for a few minutes, I'm ready to move on to something more interesting - but first, I'll just delete the `yelb` namespace to clean up the work I just did:
```command-session
kubectl delete ns yelb
namespace "yelb" deleted
```shell
kubectl delete ns yelb # [tl! .cmd]
namespace "yelb" deleted # [tl! .nocopy]
```
Now let's move on and try to deploy `yelb` behind a `LoadBalancer` service so it will get its own IP. William has a [deployment spec](https://github.com/lamw/vmware-k8s-app-demo/blob/master/yelb-lb.yaml) for that too.
```command-session
kubectl create ns yelb
namespace/yelb created
```
```command-session
kubectl apply -f https://raw.githubusercontent.com/lamw/vmware-k8s-app-demo/master/yelb-lb.yaml
service/redis-server created
```shell
kubectl create ns yelb # [tl! .cmd]
namespace/yelb created # [tl! .nocopy:1]
kubectl apply -f https://raw.githubusercontent.com/lamw/vmware-k8s-app-demo/master/yelb-lb.yaml # [tl! .cmd]
service/redis-server created # [tl! .nocopy:8]
service/yelb-db created
service/yelb-appserver created
service/yelb-ui created
@ -468,10 +451,9 @@ deployment.apps/yelb-ui created
deployment.apps/redis-server created
deployment.apps/yelb-db created
deployment.apps/yelb-appserver created
```
```command-session
kubectl -n yelb get pods
NAME READY STATUS RESTARTS AGE
kubectl -n yelb get pods # [tl! .cmd]
NAME READY STATUS RESTARTS AGE # [tl! .nocopy:4]
redis-server-74556bbcb7-q6l62 1/1 Running 0 7s
yelb-appserver-d584bb889-p5qgd 1/1 Running 0 7s
yelb-db-694586cd78-hjtn4 1/1 Running 0 7s
@ -479,9 +461,9 @@ yelb-ui-8f54fd88c-pm9qw 1/1 Running 0 7s
```
And I can take a look at that service...
```command-session
kubectl -n yelb get svc/yelb-ui
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
```shell
kubectl -n yelb get svc/yelb-ui # [tl! .cmd]
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE # [tl! .nocopy:1]
yelb-ui LoadBalancer 100.67.177.185 <pending> 80:32339/TCP 15s
```
@ -492,25 +474,23 @@ Wait a minute. That external IP is *still* `<pending>`. What gives? Oh yeah I ne
#### Deploying `kube-vip` as a load balancer
Fortunately, William Lam [wrote up some tips](https://williamlam.com/2021/10/quick-tip-install-kube-vip-as-service-load-balancer-with-tanzu-community-edition-tce.html) for handling that too. It's [based on work by Scott Rosenberg](https://github.com/vrabbi/tkgm-customizations). The quick-and-dirty steps needed to make this work are:
```command
git clone https://github.com/vrabbi/tkgm-customizations.git
```shell
git clone https://github.com/vrabbi/tkgm-customizations.git # [tl! .cmd:3]
cd tkgm-customizations/carvel-packages/kube-vip-package
kubectl apply -n tanzu-package-repo-global -f metadata.yml
kubectl apply -n tanzu-package-repo-global -f package.yaml
```
```command-session
cat << EOF > values.yaml
cat << EOF > values.yaml # [tl! .cmd]
vip_range: 192.168.1.64-192.168.1.80
EOF
```
```command
tanzu package install kubevip -p kubevip.terasky.com -v 0.3.9 -f values.yaml
tanzu package install kubevip -p kubevip.terasky.com -v 0.3.9 -f values.yaml # [tl! .cmd]
```
Now I can check out the `yelb-ui` service again:
```command-session
kubectl -n yelb get svc/yelb-ui
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
```shell
kubectl -n yelb get svc/yelb-ui # [tl!.cmd]
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE # [tl! .nocopy:1]
yelb-ui LoadBalancer 100.67.177.185 192.168.1.65 80:32339/TCP 4h35m
```
@ -518,9 +498,9 @@ And it's got an IP! I can point my browser to `http://192.168.1.65` now and see:
![Successful LoadBalancer test!](yelb_loadbalancer_demo.png)
I'll keep the `kube-vip` load balancer since it'll come in handy, but I have no further use for `yelb`:
```command-session
kubectl delete ns yelb
namespace "yelb" deleted
```shell
kubectl delete ns yelb # [tl! .cmd]
namespace "yelb" deleted # [tl! .nocopy]
```
#### Persistent Volume Claims, Storage Classes, and Storage Policies
@ -533,7 +513,8 @@ Then I create a new vSphere Storage Policy called `tkg-storage-policy` which sta
![My Tanzu storage policy](storage_policy.png)
So that's the vSphere side of things sorted; now to map that back to the Kubernetes side. For that, I'll need to define a Storage Class tied to the vSphere Storage profile so I drop these details into a new file called `vsphere-sc.yaml`:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
@ -544,13 +525,14 @@ parameters:
```
And then apply it with :
```command-session
kubectl apply -f vsphere-sc.yaml
storageclass.storage.k8s.io/vsphere created
```shell
kubectl apply -f vsphere-sc.yaml # [tl! .cmd]
storageclass.storage.k8s.io/vsphere created # [tl! .nocopy]
```
I can test that I can create a Persistent Volume Claim against the new `vsphere` Storage Class by putting this in a new file called `vsphere-pvc.yaml`:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@ -567,15 +549,15 @@ spec:
```
And applying it:
```command-session
kubectl apply -f demo-pvc.yaml
persistentvolumeclaim/vsphere-demo-1 created
```shell
kubectl apply -f demo-pvc.yaml # [tl! .cmd]
persistentvolumeclaim/vsphere-demo-1 created # [tl! .nocopy]
```
I can see the new claim, and confirm that its status is `Bound`:
```command-session
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
```shell
kubectl get pvc # [tl! .cmd]
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE # [tl! .nocopy:1]
vsphere-demo-1 Bound pvc-36cc7c01-a1b3-4c1c-ba0d-dff3fd47f93b 5Gi RWO vsphere 4m25s
```
@ -583,9 +565,9 @@ And for bonus points, I can see that the container volume was created on the vSp
![Container Volume in vSphere](container_volume_in_vsphere.png)
So that's storage sorted. I'll clean up my test volume before moving on:
```command-session
kubectl delete -f demo-pvc.yaml
persistentvolumeclaim "vsphere-demo-1" deleted
```shell
kubectl delete -f demo-pvc.yaml # [tl! .cmd]
persistentvolumeclaim "vsphere-demo-1" deleted # [tl! .nocopy]
```
### A real workload - phpIPAM
@ -597,9 +579,9 @@ So I set to work exploring some containerization options, and I found [phpipam-d
To start, I'll create a new namespace to keep things tidy:
```command-session
kubectl create ns ipam
namespace/ipam created
```shell
kubectl create ns ipam # [tl! .cmd]
namespace/ipam created # [tl! .nocopy]
```
I'm going to wind up with four pods:
@ -614,7 +596,8 @@ I'll use each container's original `docker-compose` configuration and adapt that
#### phpipam-db
The phpIPAM database will live inside a MariaDB container. Here's the relevant bit from `docker-compose`:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
services:
phpipam-db:
image: mariadb:latest
@ -629,7 +612,8 @@ services:
So it will need a `Service` exposing the container's port `3306` so that other pods can connect to the database. For my immediate demo, using `type: ClusterIP` will be sufficient since all the connections will be coming from within the cluster. When I do this for real, it will need to be `type: LoadBalancer` so that the agent running on a different cluster can connect. And it will need a `PersistentVolumeClaim` so it can store the database data at `/var/lib/mysql`. It will also get passed an environment variable to set the initial `root` password on the database instance (which will be used later during the phpIPAM install to create the initial `phpipam` database).
It might look like this on the Kubernetes side:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
# phpipam-db.yaml
apiVersion: v1
kind: Service
@ -700,7 +684,8 @@ Moving on:
#### phpipam-www
This is the `docker-compose` excerpt for the web component:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
services:
phpipam-web:
image: phpipam/phpipam-www:1.5x
@ -718,7 +703,8 @@ services:
Based on that, I can see that my `phpipam-www` pod will need a container running the `phpipam/phpipam-www:1.5x` image, a `Service` of type `LoadBalancer` to expose the web interface on port `80`, a `PersistentVolumeClaim` mounted to `/phpipam/css/images/logo`, and some environment variables passed in to configure the thing. Note that the `IPAM_DATABASE_PASS` variable defines the password used for the `phpipam` user on the database (not the `root` user referenced earlier), and the `IPAM_DATABASE_WEBHOST=%` variable will define which hosts that `phpipam` database user will be able to connect from; setting it to `%` will make sure that my remote agent can connect to the database even if I don't know where the agent will be running.
Here's how I'd adapt that into a structure that Kubernetes will understand:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
# phpipam-www.yaml
apiVersion: v1
kind: Service
@ -767,7 +753,7 @@ spec:
labels:
app: phpipam-www
spec:
containers:
containers: # [tl! focus:2]
- name: phpipam-www
image: phpipam/phpipam-www:1.5x
env:
@ -792,7 +778,8 @@ spec:
#### phpipam-cron
This container has a pretty simple configuration in `docker-compose`:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
services:
phpipam-cron:
image: phpipam/phpipam-cron:1.5x
@ -805,7 +792,8 @@ services:
No exposed ports, no need for persistence - just a base image and a few variables to tell it how to connect to the database and how often to run the scans:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
# phpipam-cron.yaml
apiVersion: apps/v1
kind: Deployment
@ -838,7 +826,8 @@ spec:
#### phpipam-agent
And finally, my remote scan agent. Here's the `docker-compose`:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
services:
phpipam-agent:
container_name: phpipam-agent
@ -860,7 +849,8 @@ services:
It's got a few additional variables to make it extra-configurable, but still no need for persistence or network exposure. That `IPAM_AGENT_KEY` variable will need to get populated the appropriate key generated within the new phpIPAM deployment, but we can deal with that later.
For now, here's how I'd tell Kubernetes about it:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
# phpipam-agent.yaml
apiVersion: apps/v1
kind: Deployment
@ -905,32 +895,32 @@ spec:
#### Deployment and configuration of phpIPAM
I can now go ahead and start deploying these containers, starting with the database one (upon which all the others rely):
```command-session
kubectl apply -f phpipam-db.yaml
service/phpipam-db created
```shell
kubectl apply -f phpipam-db.yaml # [tl! .cmd]
service/phpipam-db created # [tl! .nocopy:2]
persistentvolumeclaim/phpipam-db-pvc created
deployment.apps/phpipam-db created
```
And the web server:
```command-session
kubectl apply -f phpipam-www.yaml
service/phpipam-www created
```shell
kubectl apply -f phpipam-www.yaml # [tl! .cmd]
service/phpipam-www created # [tl! .nocopy:2]
persistentvolumeclaim/phpipam-www-pvc created
deployment.apps/phpipam-www created
```
And the cron runner:
```command-session
kubectl apply -f phpipam-cron.yaml
deployment.apps/phpipam-cron created
```shell
kubectl apply -f phpipam-cron.yaml # [tl! .cmd]
deployment.apps/phpipam-cron created # [tl! .nocopy]
```
I'll hold off on the agent container for now since I'll need to adjust the configuration slightly after getting phpIPAM set up, but I will go ahead and check out my work so far:
```command-session
kubectl -n ipam get all
NAME READY STATUS RESTARTS AGE
```shell
kubectl -n ipam get all # [tl! .cmd]
NAME READY STATUS RESTARTS AGE # [tl! .nocopy:start]
pod/phpipam-cron-6c994897c4-6rsnp 1/1 Running 0 4m30s
pod/phpipam-db-5f4c47d4b9-sb5bd 1/1 Running 0 16m
pod/phpipam-www-769c95c68d-94klg 1/1 Running 0 5m59s
@ -947,7 +937,7 @@ deployment.apps/phpipam-www 1/1 1 1 5m59s
NAME DESIRED CURRENT READY AGE
replicaset.apps/phpipam-cron-6c994897c4 1 1 1 4m30s
replicaset.apps/phpipam-db-5f4c47d4b9 1 1 1 16m
replicaset.apps/phpipam-www-769c95c68d 1 1 1 5m59s
replicaset.apps/phpipam-www-769c95c68d 1 1 1 5m59s # [tl! .nocopy:end]
```
And I can point my browser to the `EXTERNAL-IP` associated with the `phpipam-www` service to see the initial setup page:
@ -977,9 +967,9 @@ I'll copy the agent code and plug it into my `phpipam-agent.yaml` file:
```
And then deploy that:
```command-session
kubectl apply -f phpipam-agent.yaml
deployment.apps/phpipam-agent created
```shell
kubectl apply -f phpipam-agent.yaml # [tl! .cmd]
deployment.apps/phpipam-agent created # [tl! .nocopy]
```
The scan agent isn't going to do anything until it's assigned to a subnet though, so now I head to **Administration > IP related management > Sections**. phpIPAM comes with a few default sections and ranges and such defined so I'll delete those and create a new one that I'll call `Lab`.

View file

@ -41,21 +41,21 @@ The host will need to be in maintenance mode in order to apply the upgrade, and
### 3. Place host in maintenance mode
I can do that by SSH'ing to the host and running:
```commandroot
esxcli system maintenanceMode set -e true
```shell
esxcli system maintenanceMode set -e true # [tl! .cmd]
```
And can confirm that it happened with:
```commandroot-session
esxcli system maintenanceMode get
Enabled
```shell
esxcli system maintenanceMode get # [tl! .cmd]
Enabled # [tl! .nocopy]
```
### 4. Identify the profile name
Because this is an *upgrade* from one major release to another rather than a simple *update*, I need to know the name of the profile which will be applied. I can identify that with:
```commandroot-session
esxcli software sources profile list -d /vmfs/volumes/nuchost-local/_Patches/VMware-ESXi-8.0-20513097-depot.zip
Name Vendor Acceptance Level Creation Time Modification Time
```shell
esxcli software sources profile list -d /vmfs/volumes/nuchost-local/_Patches/VMware-ESXi-8.0-20513097-depot.zip # [tl! .cmd]
Name Vendor Acceptance Level Creation Time Modification Time # [tl! .nocopy:3]
---------------------------- ------------ ---------------- ------------------- -----------------
ESXi-8.0.0-20513097-standard VMware, Inc. PartnerSupported 2022-09-23T18:59:28 2022-09-23T18:59:28
ESXi-8.0.0-20513097-no-tools VMware, Inc. PartnerSupported 2022-09-23T18:59:28 2022-09-23T18:59:28
@ -68,13 +68,13 @@ In this case, I'll use the `ESXi-8.0.0-20513097-standard` profile.
### 5. Install the upgrade
Now for the moment of truth:
```commandroot
esxcli software profile update -d /vmfs/volumes/nuchost-local/_Patches/VMware-ESXi-8.0-20513097-depot.zip -p ESXi-8.0.0-20513097-standard
```shell
esxcli software profile update -d /vmfs/volumes/nuchost-local/_Patches/VMware-ESXi-8.0-20513097-depot.zip -p ESXi-8.0.0-20513097-standard # [tl! .cmd]
```
When it finishes (successfully), it leaves a little message that the update won't be complete until the host is rebooted, so I'll go ahead and do that as well:
```commandroot
reboot
```shell
reboot # [tl! .cmd]
```
And then wait (oh-so-patiently) for the host to come back up.

View file

@ -16,7 +16,8 @@ Unfortunately, I found that this approach can take a long time to run and often
After further experimentation, I settled on using PowerShell to create a one-time scheduled task that would run the updates and reboot, if necessary. I also wanted the task to automatically delete itself after running to avoid cluttering up the task scheduler library - and that last item had me quite stumped until I found [this blog post with the solution](https://iamsupergeek.com/self-deleting-scheduled-task-via-powershell/).
So here's what I put together:
```powershell {linenos=true}
```powershell
# torchlight! {"lineNumbers": true}
# This can be easily pasted into a remote PowerShell session to automatically install any available updates and reboot.
# It creates a scheduled task to start the update process after a one-minute delay so that you don't have to maintain
# the session during the process (or have the session timeout), and it also sets the task to automatically delete itself 2 hours later.

View file

@ -54,32 +54,29 @@ This needs to be run directly on the vCenter appliance so you'll need to copy th
Once that's done, just execute this on your local workstation to copy the `.zip` from your `~/Downloads/` folder to the VCSA's `/tmp/` directory:
```command
scp ~/Downloads/vdt-v1.1.4.zip root@vcsa.lab.bowdre.net:/tmp/
```shell
scp ~/Downloads/vdt-v1.1.4.zip root@vcsa.lab.bowdre.net:/tmp/ # [tl! .cmd]
```
### 3. Extract
Now pop back over to an SSH session to the VCSA, extract the `.zip`, and get ready for action:
```commandroot
cd /tmp
```
```commandroot-session
```shell
cd /tmp # [tl! .cmd_root:1]
unzip vdt-v1.1.4.zip
Archive: vdt-v1.1.4.zip
Archive: vdt-v1.1.4.zip # [tl! .nocopy:5]
3557676756cffd658fd61aab5a6673269104e83c
creating: vdt-v1.1.4/
...
inflating: vdt-v1.1.4/vdt.py
```
```commandroot
cd vdt-v1.1.4/
cd vdt-v1.1.4/ # [tl! .cmd_root]
```
### 4. Execute
Now for the fun part:
```commandroot-session
python vdt.py
_________________________
```shell
python vdt.py # [tl! .cmd_root]
_________________________ # [tl! .nocopy:7]
RUNNING PULSE CHECK
Today: Sunday, August 28 19:53:00
@ -95,7 +92,7 @@ After entering the SSO password, VDT will run for a few minutes and generate an
Once the script has completed, it's time to look through the results and fix whatever can be found. As an example, here are some of the findings from my _deliberately-broken-for-the-purposes-of-this-post_ vCenter:
#### Hostname/PNID mismatch
```log {hl_lines=[8,9,23,24]}
```text
VCENTER BASIC INFO
BASIC:
Current Time: 2022-08-28 19:54:08.370889
@ -103,7 +100,7 @@ BASIC:
vCenter Load Average: 0.26, 0.19, 0.12
Number of CPUs: 2
Total Memory: 11.71
vCenter Hostname: VCSA
vCenter Hostname: VCSA # [tl! highlight:1]
vCenter PNID: vcsa.lab.bowdre.net
vCenter IP Address: 192.168.1.12
Proxy Configured: "no"
@ -118,16 +115,16 @@ DETAILS:
Number of Clusters: 1
Disabled Plugins: None
[FAIL] The hostname and PNID do not match!
[FAIL] The hostname and PNID do not match! # [tl! highlight:1]
Please see https://kb.vmware.com/s/article/2130599 for more details.
```
Silly me - I must have changed the hostname at some point, which is not generally a Thing Which Should Be done. I can quickly [consult the referenced KB](https://kb.vmware.com/s/article/2130599) to figure out how to fix my mistake using the `/opt/vmware/share/vami/vami_config_net` utility.
#### Missing DNS
```log {hl_lines=[3,4,5,12,13]}
```text
Nameserver Queries
192.168.1.5
[FAIL] DNS with UDP - unable to resolve vcsa to 192.168.1.12
[FAIL] DNS with UDP - unable to resolve vcsa to 192.168.1.12 # [tl! highlight:2]
[FAIL] Reverse DNS - unable to resolve 192.168.1.12 to vcsa
[FAIL] DNS with TCP - unable to resolve vcsa to 192.168.1.12
@ -136,13 +133,13 @@ Nameserver Queries
dig +noall +answer -x <ip> <namserver>
dig +short +tcp <fqdn> <nameserver>
RESULT: [FAIL]
RESULT: [FAIL] # [tl! highlight:1]
Please see KB: https://kb.vmware.com/s/article/54682
```
Whoops - I guess I should go recreate the appropriate DNS records.
#### Old core files
```log
```text
CORE FILE CHECK
INFO:
These core files are older than 72 hours. consider deleting them
@ -167,19 +164,19 @@ at your discretion to reduce the size of log bundles.
```
Those core files can be useful for investigating specific issues, but holding on to them long-term doesn't really do much good. _After checking to be sure I don't need them_, I can get rid of them all pretty easily like so:
```commandroot
find /storage/core/ -name "core.*" -type f -mtime +3 -exec rm {} \;
```shell
find /storage/core/ -name "core.*" -type f -mtime +3 -exec rm {} \; # [tl! .cmd_root]
```
#### NTP status
```log
```text
VC NTP CHECK
[FAIL] NTP and Host time are both disabled!
```
Oh yeah, let's turn that back on with `systemctl start ntpd`.
#### Account status
```log
```text
Root Account Check
[FAIL] Root password expires in 13 days
Please search for 'Change the Password of the Root User'
@ -187,14 +184,14 @@ Oh yeah, let's turn that back on with `systemctl start ntpd`.
```
That's a good thing to know. I'll [take care of that](https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.vcenter.configuration.doc/GUID-48BAF973-4FD3-4FF3-B1B6-5F7286C9B59A.html) while I'm thinking about it.
```commandroot
chage -M -1 -E -1 root
```shell
chage -M -1 -E -1 root # [tl! .cmd_root]
```
#### Recheck
Now that I've corrected these issues, I can run VDT again to confirm that everything is back in a good state:
```log {hl_lines=[8,9,"25-27",32,35,"55-56",59]}
```text {hl_lines=[8,9,"25-27",32,35,"55-56",59]}
VCENTER BASIC INFO
BASIC:
Current Time: 2022-08-28 20:13:25.192503
@ -202,7 +199,7 @@ Now that I've corrected these issues, I can run VDT again to confirm that everyt
vCenter Load Average: 0.28, 0.14, 0.10
Number of CPUs: 2
Total Memory: 11.71
vCenter Hostname: vcsa.lab.bowdre.net
vCenter Hostname: vcsa.lab.bowdre.net # [tl! highlight:1]
vCenter PNID: vcsa.lab.bowdre.net
vCenter IP Address: 192.168.1.12
Proxy Configured: "no"
@ -219,20 +216,20 @@ DETAILS:
[...]
Nameserver Queries
192.168.1.5
[PASS] DNS with UDP - resolved vcsa.lab.bowdre.net to 192.168.1.12
[PASS] DNS with UDP - resolved vcsa.lab.bowdre.net to 192.168.1.12 # [tl! highlight:2]
[PASS] Reverse DNS - resolved 192.168.1.12 to vcsa.lab.bowdre.net
[PASS] DNS with TCP - resolved vcsa.lab.bowdre.net to 192.168.1.12
Commands used:
dig +short <fqdn> <nameserver>
dig +noall +answer -x <ip> <namserver>
dig +short +tcp <fqdn> <nameserver>
RESULT: [PASS]
RESULT: [PASS] # [tl! highlight]
[...]
CORE FILE CHECK
[PASS] Number of core files: 0
[PASS] Number of core files: 0 # [tl! highlight:1]
[PASS] Number of hprof files: 0
[...]
NTP Status Check
NTP Status Check # [tl! collapse:start]
+-----------------------------------LEGEND-----------------------------------+
| remote: NTP peer server |
| refid: server that this peer gets its time from |
@ -246,14 +243,14 @@ NTP Status Check
| + Peer selected for possible synchronization |
| Peer is a candidate for selection |
| ~ Peer is statically configured |
+----------------------------------------------------------------------------+
+----------------------------------------------------------------------------+ # [tl! collapse:end]
remote refid st t when poll reach delay offset jitter
==============================================================================
*104.171.113.34 130.207.244.240 2 u 1 64 17 16.831 -34.597 0.038
RESULT: [PASS]
RESULT: [PASS] # [tl! highlight]
[...]
Root Account Check
[PASS] Root password never expires
[PASS] Root password never expires # [tl! highlight]
```
All better!

View file

@ -28,29 +28,29 @@ I found that the quite-popular [Minimal Mistakes](https://mademistakes.com/work/
A quick `git clone` operation was sufficient to create a local copy of my new site in my Lenovo Chromebook Duet's [Linux environment](/setting-up-linux-on-a-new-lenovo-chromebook-duet-bonus-arm64-complications). That lets me easily create and edit Markdown posts or configuration files with VS Code, commit them to the local copy of the repo, and then push them back to GitHub when I'm ready to publish the changes.
In order to view the local changes, I needed to install Jekyll locally as well. I started by installing Ruby and other prerequisites:
```command
sudo apt-get install ruby-full build-essential zlib1g-dev
```shell
sudo apt-get install ruby-full build-essential zlib1g-dev # [tl! .cmd]
```
I added the following to my `~/.zshrc` file so that the gems would be installed under my home directory rather than somewhere more privileged:
```command
export GEM_HOME="$HOME/gems"
```shell
export GEM_HOME="$HOME/gems" # [tl! .cmd:1]
export PATH="$HOME/gems/bin:$PATH"
```
And then ran `source ~/.zshrc` so the change would take immediate effect.
I could then install Jekyll:
```command
gem install jekyll bundler
```shell
gem install jekyll bundler # [tl! .cmd]
```
I then `cd`ed to the local repo and ran `bundle install` to also load up the components specified in the repo's `Gemfile`.
And, finally, I can run this to start up the local Jekyll server instance:
```command-session
bundle exec jekyll serve -l --drafts
Configuration file: /home/jbowdre/projects/jbowdre.github.io/_config.yml
```shell
bundle exec jekyll serve -l --drafts # [tl! .cmd]
Configuration file: /home/jbowdre/projects/jbowdre.github.io/_config.yml # [tl! .nocopy:start]
Source: /home/jbowdre/projects/jbowdre.github.io
Destination: /home/jbowdre/projects/jbowdre.github.io/_site
Incremental build: enabled
@ -62,7 +62,7 @@ Configuration file: /home/jbowdre/projects/jbowdre.github.io/_config.yml
Auto-regeneration: enabled for '/home/jbowdre/projects/jbowdre.github.io'
LiveReload address: http://0.0.0.0:35729
Server address: http://0.0.0.0:4000
Server running... press ctrl-c to stop.
Server running... press ctrl-c to stop. # [tl! .nocopy:end]
```
And there it is!

View file

@ -12,8 +12,8 @@ tags:
- meta
---
```command
cp -a virtuallypotato.com runtimeterror.dev
```shell
cp -a virtuallypotato.com runtimeterror.dev # [tl! .cmd:2]
rm -rf virtuallypotato.com
ln -s virtuallypotato.com runtimeterror.dev
```

View file

@ -94,15 +94,15 @@ Wouldn't it be great if the VMs that are going to be deployed on those `1610`, `
After logging in to the VM, I entered the router's configuration mode:
```command-session
configure
[edit]
```shell
configure # [tl! .cmd]
[edit] # [tl! .nocopy]
```
I then started with setting up the interfaces - `eth0` for the `192.168.1.0/24` network, `eth1` on the trunked portgroup, and a number of VIFs on `eth1` to handle the individual VLANs I'm interested in using.
```commandroot
set interfaces ethernet eth0 address '192.168.1.8/24'
```shell
set interfaces ethernet eth0 address '192.168.1.8/24' # [tl! .cmd_root:start]
set interfaces ethernet eth0 description 'Outside'
set interfaces ethernet eth1 mtu '9000'
set interfaces ethernet eth1 vif 1610 address '172.16.10.1/24'
@ -117,13 +117,13 @@ set interfaces ethernet eth1 vif 1630 mtu '1500'
set interfaces ethernet eth1 vif 1698 description 'VLAN 1698 for vSAN'
set interfaces ethernet eth1 vif 1698 mtu '9000'
set interfaces ethernet eth1 vif 1699 description 'VLAN 1699 for vMotion'
set interfaces ethernet eth1 vif 1699 mtu '9000'
set interfaces ethernet eth1 vif 1699 mtu '9000' # [tl! .cmd_root:end]
```
I also set up NAT for the networks that should be routable:
```commandroot
set nat source rule 10 outbound-interface 'eth0'
```shell
set nat source rule 10 outbound-interface 'eth0' # [tl! .cmd_root:start]
set nat source rule 10 source address '172.16.10.0/24'
set nat source rule 10 translation address 'masquerade'
set nat source rule 20 outbound-interface 'eth0'
@ -134,13 +134,13 @@ set nat source rule 30 source address '172.16.30.0/24'
set nat source rule 30 translation address 'masquerade'
set nat source rule 100 outbound-interface 'eth0'
set nat source rule 100 translation address 'masquerade'
set protocols static route 0.0.0.0/0 next-hop 192.168.1.1
set protocols static route 0.0.0.0/0 next-hop 192.168.1.1 # [tl! .cmd_root:end]
```
And I configured DNS forwarding:
```commandroot
set service dns forwarding allow-from '0.0.0.0/0'
```shell
set service dns forwarding allow-from '0.0.0.0/0' # [tl! .cmd_root:start]
set service dns forwarding domain 10.16.172.in-addr.arpa. server '192.168.1.5'
set service dns forwarding domain 20.16.172.in-addr.arpa. server '192.168.1.5'
set service dns forwarding domain 30.16.172.in-addr.arpa. server '192.168.1.5'
@ -148,13 +148,13 @@ set service dns forwarding domain lab.bowdre.net server '192.168.1.5'
set service dns forwarding listen-address '172.16.10.1'
set service dns forwarding listen-address '172.16.20.1'
set service dns forwarding listen-address '172.16.30.1'
set service dns forwarding name-server '192.168.1.1'
set service dns forwarding name-server '192.168.1.1' # [tl! .cmd_root:end]
```
Finally, I also configured VyOS's DHCP server so that I won't have to statically configure the networking for VMs deployed from vRA:
```commandroot
set service dhcp-server shared-network-name SCOPE_10_MGMT authoritative
```shell
set service dhcp-server shared-network-name SCOPE_10_MGMT authoritative # [tl! .cmd_root:start]
set service dhcp-server shared-network-name SCOPE_10_MGMT subnet 172.16.10.0/24 default-router '172.16.10.1'
set service dhcp-server shared-network-name SCOPE_10_MGMT subnet 172.16.10.0/24 dns-server '192.168.1.5'
set service dhcp-server shared-network-name SCOPE_10_MGMT subnet 172.16.10.0/24 domain-name 'lab.bowdre.net'
@ -174,7 +174,7 @@ set service dhcp-server shared-network-name SCOPE_30_SERVERS subnet 172.16.30.0/
set service dhcp-server shared-network-name SCOPE_30_SERVERS subnet 172.16.30.0/24 domain-name 'lab.bowdre.net'
set service dhcp-server shared-network-name SCOPE_30_SERVERS subnet 172.16.30.0/24 lease '86400'
set service dhcp-server shared-network-name SCOPE_30_SERVERS subnet 172.16.30.0/24 range 0 start '172.16.30.100'
set service dhcp-server shared-network-name SCOPE_30_SERVERS subnet 172.16.30.0/24 range 0 stop '172.16.30.200'
set service dhcp-server shared-network-name SCOPE_30_SERVERS subnet 172.16.30.0/24 range 0 stop '172.16.30.200' # [tl! .cmd_root:end]
```
Satisfied with my work, I ran the `commit` and `save` commands. BOOM, this server jockey just configured a router!
@ -211,9 +211,9 @@ I migrated the physical NICs and `vmk0` to the new dvSwitch, and then created ne
I then ssh'd into the hosts and used `vmkping` to make sure they could talk to each other over these interfaces. I changed the vMotion interface to use the vMotion TCP/IP stack so needed to append the `-S vmotion` flag to the command:
```commandroot-session
vmkping -I vmk1 172.16.98.22
PING 172.16.98.22 (172.16.98.22): 56 data bytes
```shell
vmkping -I vmk1 172.16.98.22 # [tl! .cmd_root]
PING 172.16.98.22 (172.16.98.22): 56 data bytes # [tl! .nocopy:start]
64 bytes from 172.16.98.22: icmp_seq=0 ttl=64 time=0.243 ms
64 bytes from 172.16.98.22: icmp_seq=1 ttl=64 time=0.260 ms
64 bytes from 172.16.98.22: icmp_seq=2 ttl=64 time=0.262 ms
@ -221,17 +221,16 @@ PING 172.16.98.22 (172.16.98.22): 56 data bytes
--- 172.16.98.22 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.243/0.255/0.262 ms
```
```commandroot-session
vmkping -I vmk2 172.16.99.22 -S vmotion
PING 172.16.99.22 (172.16.99.22): 56 data bytes
# [tl! .nocopy:end]
vmkping -I vmk2 172.16.99.22 -S vmotion # [tl! .cmd_root]
PING 172.16.99.22 (172.16.99.22): 56 data bytes # [tl! .nocopy:start]
64 bytes from 172.16.99.22: icmp_seq=0 ttl=64 time=0.202 ms
64 bytes from 172.16.99.22: icmp_seq=1 ttl=64 time=0.312 ms
64 bytes from 172.16.99.22: icmp_seq=2 ttl=64 time=0.242 ms
--- 172.16.99.22 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.202/0.252/0.312 ms
round-trip min/avg/max = 0.202/0.252/0.312 ms # [tl! .nocopy:end]
```
Okay, time to throw some vSAN on these hosts. Select the cluster object, go to the configuration tab, scroll down to vSAN, and click "Turn on vSAN". This will be a single site cluster, and I don't need to enable any additional services. When prompted, I claim the 8GB drives for the cache tier and the 16GB drives for capacity.

View file

@ -25,7 +25,8 @@ So this will generate a name that looks something like `[user]_[catalog_item]_[s
That does mean that I'll need to add another vRO call, but I can set this up so that it only gets triggered once, when the form loads, instead of refreshing each time the inputs change.
So I hop over to vRO and create a new action, which I call `getTimestamp`. It doesn't require any inputs, and returns a single string. Here's the code:
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: getTimestamp action
// Inputs: None
// Returns: result (String)

View file

@ -58,7 +58,8 @@ I also went ahead and specified that the action will return a String.
And now for the code. I really just want to mash all those variables together into a long string, and I'll also add a timestamp to make sure each deployment name is truly unique.
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: createDeploymentName
// Inputs: catalogItemName (String), requestedByName (String), siteCode (String),
// envCode (String), functionCode (String), appCode (String)
@ -126,7 +127,8 @@ This gets filed under the existing `CustomProvisioning` folder, and I name it `n
I created a new action named (appropriately) `getNetworksForSite`. This will accept `siteCode (String)` as its input from the Service Broker request form, and will return an array of strings containing the available networks.
![getNetworksForSite action](IdrT-Un8H1.png)
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: getNetworksForSite
// Inputs: siteCode (String)
// Returns: site.value (Array/String)
@ -163,7 +165,8 @@ inputs:
and update the resource configuration for the network entity to constrain it based on `input.network` instead of `input.site` as before:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
resources:
Cloud_vSphere_Machine_1:
type: Cloud.vSphere.Machine

View file

@ -57,7 +57,8 @@ Now it's time to leave the Infrastructure tab and visit the Design one, where I'
![My first Cloud Template!](RtMljqM9x.png)
VMware's got a [pretty great document](https://docs.vmware.com/en/vRealize-Automation/8.3/Using-and-Managing-Cloud-Assembly/GUID-6BA1DA96-5C20-44BF-9C81-F8132B9B4872.html#list-of-input-properties-2) describing the syntax for these input properties, plus a lot of it is kind of self-explanatory. Let's step through this real quick:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
formatVersion: 1
inputs:
# Image Mapping
@ -73,7 +74,8 @@ inputs:
The first input is going to ask the user to select the desired Operating System for this deployment. The `oneOf` type will be presented as a dropdown (with only one option in this case, but I'll leave it this way for future flexibility); the user will see the friendly "Windows Server 2019" `title` which is tied to the `ws2019` `const` value. For now, I'll also set the `default` value of the field so I don't have to actually click the dropdown each time I test the deployment.
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
# Flavor Mapping
size:
title: Resource Size
@ -92,7 +94,8 @@ Now I'm asking the user to pick the t-shirt size of the VM. These will correspon
The `resources` section is where the data from the inputs gets applied to the deployment:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
resources:
Cloud_vSphere_Machine_1:
type: Cloud.vSphere.Machine
@ -112,7 +115,8 @@ So I'm connecting the selected `input.image` to the Image Mapping configured in
All together now:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
formatVersion: 1
inputs:
# Image Mapping
@ -188,7 +192,8 @@ I'll also use the `net:bow` and `net:dre` tags to logically divide up the networ
I can now add an input to the Cloud Template so the user can pick which site they need to deploy to:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
inputs:
# Datacenter location
site:
@ -204,7 +209,8 @@ I'm using the `enum` option now instead of `oneOf` since the site names shouldn'
And then I'll add some `constraints` to the `resources` section, making use of the `to_lower` function from the [cloud template expression syntax](https://docs.vmware.com/en/vRealize-Automation/8.3/Using-and-Managing-Cloud-Assembly/GUID-12F0BC64-6391-4E5F-AA48-C5959024F3EB.html) to automatically convert the selected site name from all-caps to lowercase so it matches the appropriate tag:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
resources:
Cloud_vSphere_Machine_1:
type: Cloud.vSphere.Machine

View file

@ -40,7 +40,8 @@ Since I try to keep things modular, I'm going to write a new vRO action within t
It's basically going to loop through the Active Directory hosts defined in vRO and search each for a matching computer name. Here's the full code:
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: checkForAdConflict action
// Inputs: computerName (String)
// Outputs: (Boolean)
@ -65,7 +66,8 @@ Now I can pop back over to my massive `Generate unique hostname` workflow and dr
I'm using this as a scriptable task so that I can do a little bit of processing before I call the action I created earlier - namely, if `conflict (Boolean)` was already set, the task should skip any further processing. That does mean that I'll need to call the action by both its module and name using `System.getModule("net.bowdre.utility").checkForAdConflict(candidateVmName)`. So here's the full script:
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: check for AD conflict task
// Inputs: candidateVmName (String), conflict (Boolean)
// Outputs: conflict (Boolean)
@ -103,22 +105,23 @@ Luckily, vRO does provide a way to import scripts bundled with their required mo
I start by creating a folder to store the script and needed module, and then I create the required `handler.ps1` file.
```command
mkdir checkDnsConflicts
```shell
mkdir checkDnsConflicts # [tl! .cmd:2]
cd checkDnsConflicts
touch handler.ps1
```
I then create a `Modules` folder and install the DnsClient-PS module:
```command
mkdir Modules
```shell
mkdir Modules # [tl! .cmd:1]
pwsh -c "Save-Module -Name DnsClient-PS -Path ./Modules/ -Repository PSGallery"
```
And then it's time to write the PowerShell script in `handler.ps1`:
```powershell {linenos=true}
```powershell
# torchlight! {"lineNumbers": true}
# PowerShell: checkForDnsConflict script
# Inputs: $inputs.hostname (String), $inputs.domain (String)
# Outputs: $queryresult (String)
@ -147,9 +150,9 @@ function handler {
Now to package it up in a `.zip` which I can then import into vRO:
```command-session
zip -r --exclude=\*.zip -X checkDnsConflicts.zip .
adding: Modules/ (stored 0%)
```shell
zip -r --exclude=\*.zip -X checkDnsConflicts.zip . # [tl! .cmd]
adding: Modules/ (stored 0%) # [tl! .nocopy:start]
adding: Modules/DnsClient-PS/ (stored 0%)
adding: Modules/DnsClient-PS/1.0.0/ (stored 0%)
adding: Modules/DnsClient-PS/1.0.0/Public/ (stored 0%)
@ -170,10 +173,9 @@ zip -r --exclude=\*.zip -X checkDnsConflicts.zip .
adding: Modules/DnsClient-PS/1.0.0/DnsClient-PS.Format.ps1xml (deflated 80%)
adding: Modules/DnsClient-PS/1.0.0/DnsClient-PS.psd1 (deflated 59%)
adding: handler.ps1 (deflated 49%)
```
```command-session
ls
checkDnsConflicts.zip handler.ps1 Modules
# [tl! .nocopy:end]
ls # [tl! .cmd]
checkDnsConflicts.zip handler.ps1 Modules # [tl! .nocopy]
```
#### checkForDnsConflict action (Deprecated)
@ -190,7 +192,8 @@ Just like with the `check for AD conflict` action, I'll add this onto the workfl
_[Update] The below script has been altered to drop the unneeded call to my homemade `checkForDnsConflict` action and instead use the built-in `System.resolveHostName()`. Thanks @powertim!_
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: check for DNS conflict
// Inputs: candidateVmName (String), conflict (Boolean), requestProperties (Properties)
// Outputs: conflict (Boolean)

View file

@ -37,7 +37,8 @@ I'll start by adding those fields as inputs on my cloud template.
I already have a `site` input at the top of the template, used for selecting the deployment location. I'll leave that there:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
inputs:
site:
type: string
@ -49,7 +50,8 @@ inputs:
I'll add the rest of the naming components below the prompts for image selection and size, starting with a dropdown of environments to pick from:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
environment:
type: string
title: Environment
@ -62,7 +64,8 @@ I'll add the rest of the naming components below the prompts for image selection
And a dropdown for those function options:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
function:
type: string
title: Function Code
@ -82,7 +85,8 @@ And a dropdown for those function options:
And finally a text entry field for the application descriptor. Note that this one includes the `minLength` and `maxLength` constraints to enforce the three-character format.
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
app:
type: string
title: Application Code
@ -95,7 +99,8 @@ And finally a text entry field for the application descriptor. Note that this on
I then need to map these inputs to the resource entity at the bottom of the template so that they can be passed to vRO as custom properties. All of these are direct mappings except for `environment` since I only want the first letter. I use the `substring()` function to achieve that, but wrap it in a conditional so that it won't implode if the environment hasn't been picked yet. I'm also going to add in a `dnsDomain` property that will be useful later when I need to query for DNS conflicts.
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
resources:
Cloud_vSphere_Machine_1:
type: Cloud.vSphere.Machine
@ -111,7 +116,8 @@ resources:
So here's the complete template:
```yaml {linenos=true}
```yaml
# torchlight! {"lineNumbers": true}
formatVersion: 1
inputs:
site:
@ -228,7 +234,8 @@ The first thing I'll want this workflow to do (particularly for testing) is to t
This action has a single input, a `Properties` object named `payload`. (By the way, vRO is pretty particular about variable typing so going forward I'll reference variables as `variableName (type)`.) Here's the JavaScript that will basically loop through each element and write the contents to the vRO debug log:
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: logPayloadProperties
// Inputs: payload (Properties)
// Outputs: none
@ -291,7 +298,8 @@ Anyway, I drop a Scriptable Task item onto the workflow canvas to handle parsing
The script for this is pretty straight-forward:
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: parse payload
// Inputs: inputProperties (Properties)
// Outputs: requestProperties (Properties), originalNames (Array/string)
@ -333,7 +341,8 @@ Select **Output** at the top of the *New Variable* dialog and the complete the f
And here's the script for that task:
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: Apply new names
// Inputs: inputProperties (Properties), newNames (Array/string)
// Outputs: resourceNames (Array/string)
@ -363,7 +372,8 @@ Okay, on to the schema. This workflow may take a little while to execute, and it
The script is very short:
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: create lock
// Inputs: lockOwner (String), lockId (String)
// Outputs: none
@ -377,7 +387,8 @@ We're getting to the meat of the operation now - another scriptable task named `
![Task: generate hostnameBase](XATryy20y.png)
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: generate hostnameBase
// Inputs: nameFormat (String), requestProperties (Properties), baseFormat (String)
// Outputs: hostnameBase (String), digitCount (Number), hostnameSeq (Number)
@ -415,7 +426,8 @@ I've only got the one vCenter in my lab. At work, I've got multiple vCenters so
Anyway, back to my "Generate unique hostname" workflow, where I'll add another scriptable task to prepare the vCenter SDK connection. This one doesn't require any inputs, but will output an array of `VC:SdkConnection` objects:
![Task: prepare vCenter SDK connection](ByIWO66PC.png)
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: prepare vCenter SDK connection
// Inputs: none
// Outputs: sdkConnections (Array/VC:SdkConnection)
@ -432,7 +444,8 @@ Next, I'm going to drop another ForEach element onto the canvas. For each vCente
That `vmsByHost (Array/array)` object contains any and all VMs which match `hostnameBase (String)`, but they're broken down by the host they're running on. So I use a scriptable task to convert that array-of-arrays into a new array-of-strings containing just the VM names.
![Task: unpack results for all hosts](gIEFRnilq.png)
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: unpack results for all hosts
// Inputs: vmsByHost (Array/Array)
// Outputs: vmNames (Array/string)
@ -453,7 +466,8 @@ vmNames = vms.map(function(i) {return (i.displayName).toUpperCase()})
This scriptable task will check the `computerNames` configuration element we created earlier to see if we've already named a VM starting with `hostnameBase (String)`. If such a name exists, we'll increment the number at the end by one, and return that as a new `hostnameSeq (Number)` variable; if it's the first of its kind, `hostnameSeq (Number)` will be set to `1`. And then we'll combine `hostnameBase (String)` and `hostnameSeq (Number)` to create the new `candidateVmName (String)`. If things don't work out, this script will throw `errMsg (String)` so I need to add that as an output exception binding as well.
![Task: generate hostnameSeq & candidateVmName](fWlSrD56N.png)
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: generate hostnameSeq & candidateVmName
// Inputs: hostnameBase (String), digitCount (Number)
// Outputs: hostnameSeq (Number), computerNames (ConfigurationElement), candidateVmName (String)
@ -500,7 +514,8 @@ System.log("Proposed VM name: " + candidateVmName)
Now that I know what I'd like to try to name this new VM, it's time to start checking for any potential conflicts. So this task will compare my `candidateVmName (String)` against the existing `vmNames (Array/string)` to see if there are any collisions. If there's a match, it will set a new variable called `conflict (Boolean)` to `true` and also report the issue through the `errMsg (String)` output exception binding. Otherwise it will move on to the next check.
![Task: check for VM name conflicts](qmHszypww.png)
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: check for VM name conflicts
// Inputs: candidateVmName (String), vmNames (Array/string)
// Outputs: conflict (Boolean)
@ -527,7 +542,8 @@ I can then drag the new element away from the "everything is fine" flow, and con
All this task really does is clear the `conflict (Boolean)` flag so that's the only output.
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: conflict resolution
// Inputs: none
// Outputs: conflict (Boolean)
@ -542,7 +558,8 @@ So if `check VM name conflict` encounters a collision with an existing VM name i
Assuming that everything has gone according to plan and the workflow has avoided any naming conflicts, it will need to return `nextVmName (String)` back to the `VM Provisioning` workflow. That's as simple as setting it to the last value of `candidateVmName (String)`:
![Task: return nextVmName](5QFTPHp5H.png)
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript: return nextVmName
// Inputs: candidateVmName (String)
// Outputs: nextVmName (String)
@ -555,7 +572,8 @@ System.log(" ***** Selecting [" + nextVmName + "] as the next VM name ***** ")
And we should also remove that lock that we created at the start of this workflow.
![Task: remove lock](BhBnBh8VB.png)
```js {linenos=true}
```javascript
// torchlight! {"lineNumbers": true}
// JavaScript remove lock
// Inputs: lockId (String), lockOwner (String)
// Outputs: none