diff --git a/.ansible-lint b/.ansible-lint index 659b361..f7b9b7e 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -7,6 +7,7 @@ mock_modules: - orka_image_list - orka_image_push - orka_image_delete + - network_setup exclude_paths: - roles/install_engine/tasks/main.yml diff --git a/.ansible/modules/network_setup.py b/.ansible/modules/network_setup.py new file mode 100644 index 0000000..610df9b --- /dev/null +++ b/.ansible/modules/network_setup.py @@ -0,0 +1,32 @@ +# This is a mocked Ansible module generated by ansible-lint +from ansible.module_utils.basic import AnsibleModule + +DOCUMENTATION = ''' +module: network_setup + +short_description: Mocked +version_added: "1.0.0" +description: Mocked + +author: + - ansible-lint (@nobody) +''' +EXAMPLES = '''mocked''' +RETURN = '''mocked''' + + +def main(): + result = dict( + changed=False, + original_message='', + message='') + + module = AnsibleModule( + argument_spec=dict(), + supports_check_mode=True, + ) + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/README.md b/README.md index e10d774..da6c82b 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,30 @@ Optional variables: - `uninstall_engine_data` (default `true`) — remove `/opt/orka` (engine data, state, and logs). Set to `false` to preserve VM state across reinstalls. +#### Configure Hosts for Harbor (MSDC) + +When running at MacStadium (MSDC) and using Harbor for OCI image storage, the Mac hosts must be configured to reach Harbor. + +The following variables are required: + +- `msdc_storage_network`: The first 3 octets of the storage network, e.g. 172.16.180 +- `msdc_storage_vlan`: The VLAN tag, e.g. 2870 +- `configure_harbor_msdc_region`: The MSDC region, must be one of 'atl', 'las' or 'dub' +- `configure_harbor_msdc_domain`: The domain for Harbor, e.g. my-harbor-01.oci.las1.macstadiumcloud.com +- `configure_harbor_msdc_ip`: The IP address for the Harbor server, e.g. 10.221.189.254 + +To configure the hosts, run: + +```bash +ansible-playbook configure_harbor_msdc.yml -i dev/inventory +``` + +**NOTE**: To completely remove the Harbor configuration and storage VLAN interface, run: + +```bash +ansible-playbook configure_harbor_msdc.yml -i dev/inventory -e "configure_storage_interface_msdc_remove=true" -e "configure_harbor_msdc_remove=true" +``` + #### Install Android SDK To install the Android SDK and AVD runtime tooling on target hosts: diff --git a/configure_harbor_msdc.yml b/configure_harbor_msdc.yml new file mode 100644 index 0000000..11c0ef2 --- /dev/null +++ b/configure_harbor_msdc.yml @@ -0,0 +1,17 @@ +--- +- name: Configure access for Harbor on target hosts at MSDC + hosts: hosts + gather_facts: false + module_defaults: + ansible.builtin.setup: + gather_timeout: 20 + pre_tasks: + - name: Gather minimal facts + ansible.builtin.setup: + filter: + - "ansible_default_ipv4" + tags: + - always + roles: + - configure_storage_interface_msdc + - configure_harbor_msdc diff --git a/library/network_setup.py b/library/network_setup.py new file mode 100644 index 0000000..a2345ae --- /dev/null +++ b/library/network_setup.py @@ -0,0 +1,272 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +DOCUMENTATION = r""" +module: network_setup +short_description: Sets up a VLAN +description: + - Configure a VLAN and network service +options: + name: + description: + - VLAN name + type: string + required: true + device: + description: + - The device used (en0, en1, etc...) + type: string + required: false + tag: + description: + - The VLAN tag + type: string + required: false + ip: + description: + - IP of the service + type: str + required: false + mask: + description: + - Mask of the service + type: str + required: false + router: + description: + - Router of the service + type: str + required: false + state: + description: + - Add, delete, enable or disable the interface + type: str + choices: [ present, absent, enable, disable ] + default: present + force: + description: + - Apply the change regardless of current state + type: bool + default: false +author: +- Ivan Spasov (@ispasov) +- Spike Burton (@spikeburton) +""" + +EXAMPLES = r""" +- name: Add a storage vlan + network_setup: + name: storage + device: en0 + tag: 2339 + ip: 10.172.1.12 + mask: 255.255.254.0 + router: 10.172.1.11 +- name: Remove a storage vlan + network_setup: + name: storage + device: en0 + tag: 2339 + ip: 10.172.1.12 + mask: 255.255.254.0 + router: 10.172.1.11 + state: absent +- name: Force recreate a storage vlan + network_setup: + name: storage + device: en0 + tag: 2339 + ip: 10.172.1.12 + mask: 255.255.254.0 + router: 10.172.1.11 + force: true +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text + + +class NetworkServiceModuleError(Exception): + pass + + +class NetworkService(object): + def __init__(self, module): + self.module = module + self.name = module.params["name"] + self.device = module.params["device"] + self.tag = module.params["tag"] + self.ip = module.params["ip"] + self.mask = module.params["mask"] + self.router = module.params["router"] + self.state = module.params["state"] + self.force = module.params["force"] + + def execute_command(self, cmd): + cmd = [to_text(item) for item in cmd] + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + raise NetworkServiceModuleError(out + err) + + return out + + def vlan_exists(self): + out = self.execute_command(["networksetup", "-listVlans"]) + + return f"VLAN User Defined Name: {self.name}" in out.splitlines() + + def create(self): + self.create_vlan() + self.configure_service() + self.configure_dns() + + def create_vlan(self): + cmd = ["networksetup", "-createVLAN", self.name, self.device, self.tag] + self.execute_command(cmd) + + def configure_service(self): + # An empty router leaves the service without a default gateway. Directly + # connected subnets (e.g. the MSDC storage VLAN) don't need one, and + # omitting it avoids ever competing for the host's default route. + cmd = [ + "networksetup", + "-setmanual", + f"{self.name} Configuration", + self.ip, + self.mask, + self.router or "", + ] + self.execute_command(cmd) + + def configure_dns(self): + cmd = [ + "networksetup", + "-setdnsservers", + f"{self.name} Configuration", + "8.8.8.8", + "1.1.1.1", + ] + self.execute_command(cmd) + + def vlan_changed(self): + out = self.execute_command(["networksetup", "-listVlans"]) + vlan_lines = out.splitlines() + vlan_name_index = vlan_lines.index(f"VLAN User Defined Name: {self.name}") + vlan_device = ( + vlan_lines[vlan_name_index + 1].replace("Parent Device:", "").strip() + ) + vlan_tag = vlan_lines[vlan_name_index + 3].replace("Tag:", "").strip() + + return vlan_device != self.device or vlan_tag != self.tag + + def service_changed(self): + out = self.execute_command( + ["networksetup", "-getinfo", f"{self.name} Configuration"] + ) + service_lines = out.splitlines() + service_ip = service_lines[1].replace("IP address:", "").strip() + service_mask = service_lines[2].replace("Subnet mask:", "").strip() + service_router = service_lines[3].replace("Router:", "").strip() + + return ( + service_ip != self.ip + or service_mask != self.mask + or service_router != (self.router or "") + ) + + def needs_update(self): + return self.vlan_changed() or self.service_changed() + + def delete(self): + cmd = ["networksetup", "-deleteVLAN", self.name, self.device, self.tag] + self.execute_command(cmd) + + def service_exists(self): + cmd = ["networksetup", "-listallnetworkservices"] + out = self.execute_command(cmd) + + for line in out.splitlines(): + service_name = line.lstrip("*").strip() + if service_name == self.name: + return True + return False + + def service_enabled(self): + cmd = ["networksetup", "-getnetworkserviceenabled", self.name] + out = self.execute_command(cmd) + return out.strip().lower() == "enabled" + + def enable_service(self): + cmd = ["networksetup", "-setnetworkserviceenabled", self.name, "on"] + self.execute_command(cmd) + + def disable_service(self): + cmd = ["networksetup", "-setnetworkserviceenabled", self.name, "off"] + self.execute_command(cmd) + + def set_service_state(self): + should_enable = self.state == "enable" + is_currently_enabled = self.service_enabled() + + if self.force or is_currently_enabled != should_enable: + self.enable_service() if should_enable else self.disable_service() + return True + + return False + + def run(self): + changed = False + + if self.state == "present": + if not self.vlan_exists(): + self.create() + changed = True + elif self.force or self.needs_update(): + self.delete() + self.create() + changed = True + + elif self.state == "absent" and self.vlan_exists(): + self.delete() + changed = True + + elif self.service_exists() and self.state in ["enable", "disable"]: + changed = self.set_service_state() + + return changed + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(type="str", required=True), + device=dict(type="str"), + tag=dict(type="str"), + ip=dict(type="str"), + mask=dict(type="str", default="manual"), + router=dict(type="str"), + state=dict( + type="str", + default="present", + choices=["present", "absent", "enable", "disable"], + ), + force=dict(type="bool", default=False), + ), + required_if=[ + ("state", "present", ("device", "tag", "ip", "mask")), + ("state", "absent", ("device", "tag")), + ], + supports_check_mode=False, + ) + + network_service = NetworkService(module) + try: + changed = network_service.run() + except Exception as e: + module.fail_json(msg=str(e)) + + module.exit_json(changed=changed) + + +if __name__ == "__main__": + main() diff --git a/roles/configure_harbor_msdc/defaults/main.yml b/roles/configure_harbor_msdc/defaults/main.yml new file mode 100644 index 0000000..76dc14d --- /dev/null +++ b/roles/configure_harbor_msdc/defaults/main.yml @@ -0,0 +1,8 @@ +--- +# msdc_storage_network: +# configure_harbor_msdc_region: # Should be one of 'atl', 'las' or 'dub' +# configure_harbor_msdc_domain: +# configure_harbor_msdc_ip: +configure_harbor_msdc_s3_domain: 1.obj.{{ configure_harbor_msdc_region }}1.macstadiumcloud.com +configure_harbor_msdc_s3_ip: "{{ msdc_storage_network }}.1" +configure_harbor_msdc_remove: false diff --git a/roles/configure_harbor_msdc/tasks/main.yml b/roles/configure_harbor_msdc/tasks/main.yml new file mode 100644 index 0000000..1173ddb --- /dev/null +++ b/roles/configure_harbor_msdc/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: Add entries to /etc/hosts + become: true + ansible.builtin.blockinfile: + path: /etc/hosts + marker: "{{ configure_harbor_msdc_marker }}" + block: | + {{ configure_harbor_msdc_ip }} {{ configure_harbor_msdc_domain }} + {{ configure_harbor_msdc_s3_ip }} {{ configure_harbor_msdc_s3_domain }} + when: not configure_harbor_msdc_remove | bool + +- name: Remove entries from /etc/hosts + become: true + ansible.builtin.blockinfile: + path: /etc/hosts + marker: "{{ configure_harbor_msdc_marker }}" + state: absent + when: configure_harbor_msdc_remove | bool diff --git a/roles/configure_harbor_msdc/vars/main.yml b/roles/configure_harbor_msdc/vars/main.yml new file mode 100644 index 0000000..744f507 --- /dev/null +++ b/roles/configure_harbor_msdc/vars/main.yml @@ -0,0 +1,2 @@ +--- +configure_harbor_msdc_marker: "# {mark} Harbor FQDNs" diff --git a/roles/configure_storage_interface_msdc/defaults/main.yml b/roles/configure_storage_interface_msdc/defaults/main.yml new file mode 100644 index 0000000..a9d777c --- /dev/null +++ b/roles/configure_storage_interface_msdc/defaults/main.yml @@ -0,0 +1,9 @@ +--- +# msdc_storage_network: "" +# msdc_storage_vlan: "" +configure_storage_interface_msdc_remove: false +configure_storage_interface_msdc_name: "storage" +configure_storage_interface_msdc_data_network_address: "{{ ansible_default_ipv4.address }}" +configure_storage_interface_msdc_storage_network_address: >- + {{ msdc_storage_network }}.{{ configure_storage_interface_msdc_data_network_address.split('.')[-1] }} +configure_storage_interface_msdc_storage_network_mask: "255.255.254.0" diff --git a/roles/configure_storage_interface_msdc/tasks/main.yml b/roles/configure_storage_interface_msdc/tasks/main.yml new file mode 100644 index 0000000..1b3bbc1 --- /dev/null +++ b/roles/configure_storage_interface_msdc/tasks/main.yml @@ -0,0 +1,17 @@ +--- +- name: Add storage VLAN + network_setup: + name: "{{ configure_storage_interface_msdc_name }}" + device: en0 + tag: "{{ msdc_storage_vlan }}" + ip: "{{ configure_storage_interface_msdc_storage_network_address }}" + mask: "{{ configure_storage_interface_msdc_storage_network_mask }}" + when: not configure_storage_interface_msdc_remove | bool + +- name: Remove storage VLAN + network_setup: + name: "{{ configure_storage_interface_msdc_name }}" + device: en0 + tag: "{{ msdc_storage_vlan }}" + state: absent + when: configure_storage_interface_msdc_remove | bool diff --git a/semaphore/project-seed.json b/semaphore/project-seed.json index b8bd29a..ff86a4c 100644 --- a/semaphore/project-seed.json +++ b/semaphore/project-seed.json @@ -310,6 +310,14 @@ } ] }, + { + "name": "Hosts | Configure Hosts for Harbor (MSDC)", + "app": "ansible", + "playbook": "configure_harbor_msdc.yml", + "repository": "Local Playbooks", + "inventory": "Dev Inventory", + "environment": "Default" + }, { "name": "Hosts | Uninstall Android SDK Components", "app": "ansible",