Skip to content

Commit

Permalink
⤵️ automation(synchronize) Applying changes from upstream repository.
Browse files Browse the repository at this point in the history
  • Loading branch information
Megabyte Labs Automation committed Dec 29, 2021
1 parent 0892b34 commit 357703b
Show file tree
Hide file tree
Showing 21 changed files with 652 additions and 158 deletions.
7 changes: 6 additions & 1 deletion .config/molecule/config.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
---
dependency:
name: shell
command: task ansible:test:molecule:dependencies
command: |
if type task > /dev/null; then
task ansible:test:molecule:dependencies
else
ansible-galaxy install --ignore-errors -r requirements.yml
fi
provisioner:
name: ansible
options:
Expand Down
224 changes: 224 additions & 0 deletions .config/molecule/files/windows_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
#!/usr/bin/env python

# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import base64
import copy
import datetime
import json
import time
import argparse

# PyCrypto library: https://pypi.python.org/pypi/pycrypto
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
from Crypto.Util.number import long_to_bytes

# Google API Client Library for Python:
# https://developers.google.com/api-client-library/python/start/get_started
import google.auth
from googleapiclient.discovery import build


def GetCompute():
"""Get a compute object for communicating with the Compute Engine API."""
credentials, project = google.auth.default()
compute = build("compute", "v1", credentials=credentials)
return compute


def GetInstance(compute, instance, zone, project):
"""Get the data for a Google Compute Engine instance."""
cmd = compute.instances().get(instance=instance, project=project, zone=zone)
return cmd.execute()


def GetKey():
"""Get an RSA key for encryption."""
# This uses the PyCrypto library
key = RSA.generate(2048)
return key


def GetModulusExponentInBase64(key):
"""Return the public modulus and exponent for the key in bas64 encoding."""
mod = long_to_bytes(key.n)
exp = long_to_bytes(key.e)

modulus = base64.b64encode(mod)
exponent = base64.b64encode(exp)

return modulus, exponent


def GetExpirationTimeString():
"""Return an RFC3339 UTC timestamp for 5 minutes from now."""
utc_now = datetime.datetime.utcnow()
# These metadata entries are one-time-use, so the expiration time does
# not need to be very far in the future. In fact, one minute would
# generally be sufficient. Five minutes allows for minor variations
# between the time on the client and the time on the server.
expire_time = utc_now + datetime.timedelta(minutes=5)
return expire_time.strftime("%Y-%m-%dT%H:%M:%SZ")


def GetJsonString(user, modulus, exponent, email):
"""Return the JSON string object that represents the windows-keys entry."""

converted_modulus = modulus.decode("utf-8")
converted_exponent = exponent.decode("utf-8")

expire = GetExpirationTimeString()
data = {
"userName": user,
"modulus": converted_modulus,
"exponent": converted_exponent,
"email": email,
"expireOn": expire,
}

return json.dumps(data)


def UpdateWindowsKeys(old_metadata, metadata_entry):
"""Return updated metadata contents with the new windows-keys entry."""
# Simply overwrites the "windows-keys" metadata entry. Production code may
# want to append new lines to the metadata value and remove any expired
# entries.
new_metadata = copy.deepcopy(old_metadata)
new_metadata["items"] = [{"key": "windows-keys", "value": metadata_entry}]
return new_metadata


def UpdateInstanceMetadata(compute, instance, zone, project, new_metadata):
"""Update the instance metadata."""
cmd = compute.instances().setMetadata(
instance=instance, project=project, zone=zone, body=new_metadata
)
return cmd.execute()


def GetSerialPortFourOutput(compute, instance, zone, project):
"""Get the output from serial port 4 from the instance."""
# Encrypted passwords are printed to COM4 on the windows server:
port = 4
cmd = compute.instances().getSerialPortOutput(
instance=instance, project=project, zone=zone, port=port
)
output = cmd.execute()
return output["contents"]


def GetEncryptedPasswordFromSerialPort(serial_port_output, modulus):
"""Find and return the correct encrypted password, based on the modulus."""
# In production code, this may need to be run multiple times if the output
# does not yet contain the correct entry.

converted_modulus = modulus.decode("utf-8")

output = serial_port_output.split("\n")
for line in reversed(output):
try:
entry = json.loads(line)
if converted_modulus == entry["modulus"]:
return entry["encryptedPassword"]
except ValueError:
pass


def DecryptPassword(encrypted_password, key):
"""Decrypt a base64 encoded encrypted password using the provided key."""

decoded_password = base64.b64decode(encrypted_password)
cipher = PKCS1_OAEP.new(key)
password = cipher.decrypt(decoded_password)
return password


def Arguments():
# Create the parser
args = argparse.ArgumentParser(description="List the content of a folder")

# Add the arguments
args.add_argument(
"--instance", metavar="instance", type=str, help="compute instance name"
)

args.add_argument("--zone", metavar="zone", type=str, help="compute zone")

args.add_argument("--project", metavar="project", type=str, help="gcp project")

args.add_argument("--username", metavar="username", type=str, help="username")

args.add_argument("--email", metavar="email", type=str, help="email")

# return arguments
return args.parse_args()


def main():
config_args = Arguments()

# Setup
compute = GetCompute()
key = GetKey()
modulus, exponent = GetModulusExponentInBase64(key)

# Get existing metadata
instance_ref = GetInstance(
compute, config_args.instance, config_args.zone, config_args.project
)
old_metadata = instance_ref["metadata"]
# Create and set new metadata
metadata_entry = GetJsonString(
config_args.username, modulus, exponent, config_args.email
)
new_metadata = UpdateWindowsKeys(old_metadata, metadata_entry)

# Get Serial output BEFORE the modification
serial_port_output = GetSerialPortFourOutput(
compute, config_args.instance, config_args.zone, config_args.project
)

UpdateInstanceMetadata(
compute,
config_args.instance,
config_args.zone,
config_args.project,
new_metadata,
)

# Get and decrypt password from serial port output
# Monitor changes from output to get the encrypted password as soon as it's generated, will wait for 30 seconds
i = 0
new_serial_port_output = serial_port_output
while i <= 20 and serial_port_output == new_serial_port_output:
new_serial_port_output = GetSerialPortFourOutput(
compute, config_args.instance, config_args.zone, config_args.project
)
i += 1
time.sleep(3)

enc_password = GetEncryptedPasswordFromSerialPort(new_serial_port_output, modulus)

password = DecryptPassword(enc_password, key)
converted_password = password.decode("utf-8")

# Display only the password
print(format(converted_password))


if __name__ == "__main__":
main()
51 changes: 51 additions & 0 deletions .config/molecule/handlers/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
# yamllint disable rule:line-length
- name: Populate instance config dict Linux
ansible.builtin.set_fact:
instance_conf_dict:
instance: '{{ instance_info.name }}'
# eslint-disable-next-line max-len
address: '{{ instance_info.networkInterfaces.0.accessConfigs.0.natIP if molecule_yml.driver.external_access else instance_info.networkInterfaces.0.networkIP }}'
user: "{{ lookup('env','USER') }}"
port: '22'
identity_file: '{{ ssh_identity_file }}'
instance_os_type: '{{ molecule_yml.driver.instance_os_type }}'

loop: '{{ server.results }}'
loop_control:
loop_var: instance_info
no_log: true
register: instance_conf_dict

- name: Populate instance config dict Windows
ansible.builtin.set_fact:
instance_conf_dict:
instance: '{{ instance_info.name }}'
# eslint-disable-next-line max-len
address: '{{ instance_info.networkInterfaces.0.accessConfigs.0.natIP if molecule_yml.driver.external_access else instance_info.networkInterfaces.0.networkIP }}'
user: molecule_usr
password: '{{ instance_info.password }}'
port: '{{ instance_info.winrm_port | default(5986) }}'
winrm_transport: "{{ molecule_yml.driver.winrm_transport | default('ntlm') }}"
winrm_server_cert_validation: "{{ molecule_yml.driver.winrm_server_cert_validation | default('ignore') }}"
instance_os_type: '{{ molecule_yml.driver.instance_os_type }}'

loop: '{{ win_instances }}'
loop_control:
loop_var: instance_info
no_log: true
register: instance_conf_dict

- name: Wipe out instance config
ansible.builtin.set_fact:
instance_conf: {}

- name: Convert instance config dict to a list
ansible.builtin.set_fact:
instance_conf: "{{ instance_conf_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}"

- name: Dump instance config
ansible.builtin.copy:
content: '{{ instance_conf }}'
dest: '{{ molecule_instance_config }}'
mode: '0600'
66 changes: 66 additions & 0 deletions .config/molecule/tasks/create_linux_instance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
# yamllint disable rule:line-length
- name: create ssh keypair
community.crypto.openssh_keypair:
comment: "{{ lookup('env','USER') }} user for Molecule"
path: '{{ ssh_identity_file }}'
register: keypair

- name: create molecule Linux instance(s)
google.cloud.gcp_compute_instance:
state: present
name: '{{ item.name }}'
machine_type: "{{ item.machine_type | default('n1-standard-1') }}"
metadata:
ssh-keys: "{{ lookup('env','USER') }}:{{ keypair.public_key }}"
scheduling:
preemptible: '{{ item.preemptible | default(false) }}'
disks:
- auto_delete: true
boot: true
initialize_params:
disk_size_gb: '{{ item.disk_size_gb | default(omit) }}'
source_image: "{{ item.image | default('projects/debian-cloud/global/images/family/debian-10') }}"
source_image_encryption_key:
raw_key: '{{ item.image_encryption_key | default(omit) }}'
network_interfaces:
- network:
# eslint-disable-next-line max-len
selfLink: "https://www.googleapis.com/compute/v1/projects/{{ molecule_yml.driver.vpc_host_project | default(gcp_project_id) }}/global/networks/{{ molecule_yml.driver.network_name | default('default') }}"
subnetwork:
# eslint-disable-next-line max-len
selfLink: "https://compute.googleapis.com/compute/v1/projects/{{ molecule_yml.driver.vpc_host_project | default(gcp_project_id) }}/regions/{{ molecule_yml.driver.region }}/subnetworks/{{ molecule_yml.driver.subnetwork_name | default('default') }}"
access_configs: "{{ [{'name': 'instance_ip', 'type': 'ONE_TO_ONE_NAT'}] if molecule_yml.driver.external_access else [] }}"
zone: "{{ item.zone | default(molecule_yml.driver.region + '-b') }}"
project: '{{ gcp_project_id }}'
scopes: "{{ molecule_yml.driver.scopes | default(['https://www.googleapis.com/auth/compute'], True) }}"
service_account_email: '{{ molecule_yml.driver.service_account_email | default (omit, true) }}'
service_account_file: '{{ molecule_yml.driver.service_account_file | default (omit, true) }}'
auth_kind: '{{ molecule_yml.driver.auth_kind | default(omit, true) }}'
register: async_results
loop: '{{ molecule_yml.platforms }}'
loop_control:
pause: 3
async: 7200
poll: 0

- name: Wait for instance(s) creation to complete
ansible.builtin.async_status:
jid: '{{ item.ansible_job_id }}'
loop: '{{ async_results.results }}'
register: server
until: server.finished
retries: 300
delay: 10
notify:
- Populate instance config dict Linux
- Convert instance config dict to a list
- Dump instance config

- name: Wait for SSH
ansible.builtin.wait_for:
port: 22
host: '{{ item.networkInterfaces.0.accessConfigs.0.natIP if molecule_yml.driver.external_access else item.networkInterfaces.0.networkIP }}'
search_regex: SSH
delay: 10
loop: '{{ server.results }}'
Loading

0 comments on commit 357703b

Please sign in to comment.