From fa32907c02582fc105166c33c5106ccf6b9389c6 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Fri, 14 Sep 2018 11:26:13 -0400 Subject: [PATCH] Secure Key Management This patchset implements the Secure Key Management feature as described in Gerrit 1429, enabling support for native charms deployed to machines provisioned by the Resource Orchestrator. - Implement GetPublicKey, which will provide the public key to be injected into new machines - Support machine placement, to provision an existing machine for use with juju - Automatically create a SSH keypair to be used for provisioning - Add method to check if a charm is deployed (CI) - Update integration tests to use new ssh key workflow Signed-off-by: Adam Israel Change-Id: Iacd2f02800484fd90945f9b9c1ac2d8951115a76 --- n2vc/vnf.py | 104 +++++++++++++++++++++++++++++++++------ tests/base.py | 75 +++++++++++++++++++++++++--- tests/test_ssh_keygen.py | 18 +++++++ 3 files changed, 175 insertions(+), 22 deletions(-) create mode 100644 tests/test_ssh_keygen.py diff --git a/n2vc/vnf.py b/n2vc/vnf.py index 8064cb3..a1fcfe3 100644 --- a/n2vc/vnf.py +++ b/n2vc/vnf.py @@ -3,7 +3,9 @@ import logging import os import os.path import re +import shlex import ssl +import subprocess import sys # import time @@ -333,15 +335,14 @@ class N2VC: ######################################################## to = "" if machine_spec.keys(): - # TODO: This needs to be tested. - # if all(k in machine_spec for k in ['hostname', 'username']): - # # Enlist the existing machine in Juju - # machine = await self.model.add_machine(spec='ssh:%@%'.format( - # specs['host'], - # specs['user'], - # )) - # to = machine.id - pass + if all(k in machine_spec for k in ['host', 'user']): + # Enlist an existing machine as a Juju unit + machine = await model.add_machine(spec='ssh:{}@{}:{}'.format( + machine_spec['user'], + machine_spec['host'], + self.GetPrivateKeyPath(), + )) + to = machine.id ####################################### # Get the initial charm configuration # @@ -351,9 +352,6 @@ class N2VC: if 'rw_mgmt_ip' in params: rw_mgmt_ip = params['rw_mgmt_ip'] - # initial_config = {} - # self.log.debug(type(params)) - # self.log.debug("Params: {}".format(params)) if 'initial-config-primitive' not in params: params['initial-config-primitive'] = {} @@ -382,8 +380,8 @@ class N2VC: series='xenial', # Apply the initial 'config' primitive during deployment config=initial_config, - # TBD: Where to deploy the charm to. - to=None, + # Where to deploy the charm to. + to=to, ) # ####################################### @@ -487,6 +485,77 @@ class N2VC: return results + # async def ProvisionMachine(self, model_name, hostname, username): + # """Provision machine for usage with Juju. + # + # Provisions a previously instantiated machine for use with Juju. + # """ + # try: + # if not self.authenticated: + # await self.login() + # + # # FIXME: This is hard-coded until model-per-ns is added + # model_name = 'default' + # + # model = await self.get_model(model_name) + # model.add_machine(spec={}) + # + # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format( + # "ubuntu", + # host['address'], + # private_key_path, + # )) + # return machine.id + # + # except Exception as e: + # self.log.debug( + # "Caught exception while getting primitive status: {}".format(e) + # ) + # raise N2VCPrimitiveExecutionFailed(e) + + def GetPrivateKeyPath(self): + homedir = os.environ['HOME'] + sshdir = "{}/.ssh".format(homedir) + private_key_path = "{}/id_n2vc_rsa".format(sshdir) + return private_key_path + + async def GetPublicKey(self): + """Get the N2VC SSH public key.abs + + Returns the SSH public key, to be injected into virtual machines to + be managed by the VCA. + + The first time this is run, a ssh keypair will be created. The public + key is injected into a VM so that we can provision the machine with + Juju, after which Juju will communicate with the VM directly via the + juju agent. + """ + public_key = "" + + # Find the path to where we expect our key to live. + homedir = os.environ['HOME'] + sshdir = "{}/.ssh".format(homedir) + if not os.path.exists(sshdir): + os.mkdir(sshdir) + + private_key_path = "{}/id_n2vc_rsa".format(sshdir) + public_key_path = "{}.pub".format(private_key_path) + + # If we don't have a key generated, generate it. + if not os.path.exists(private_key_path): + cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format( + "rsa", + "4096", + private_key_path + ) + subprocess.check_output(shlex.split(cmd)) + + # Read the public key + with open(public_key_path, "r") as f: + public_key = f.readline() + + return public_key + async def ExecuteInitialPrimitives(self, model_name, application_name, params, callback=None, *callback_args): """Execute multiple primitives. @@ -650,6 +719,13 @@ class N2VC: return metrics + async def HasApplication(self, model_name, application_name): + model = await self.get_model(model_name) + app = await self.get_application(model, application_name) + if app: + return True + return False + # Non-public methods async def add_relation(self, a, b, via=None): """ diff --git a/tests/base.py b/tests/base.py index 1db4843..0959059 100644 --- a/tests/base.py +++ b/tests/base.py @@ -163,7 +163,7 @@ def create_lxd_container(public_key=None, name="test_name"): name, ) - private_key_path, public_key_path = find_juju_ssh_keys() + private_key_path, public_key_path = find_n2vc_ssh_keys() try: # create profile w/cloud-init and juju ssh key @@ -330,6 +330,21 @@ def find_lxd_config(): return (None, None) +def find_n2vc_ssh_keys(): + """Find the N2VC ssh keys.""" + + paths = [] + paths.append(os.path.expanduser("~/.ssh/")) + + for path in paths: + if os.path.exists(path): + private = os.path.expanduser("{}/id_n2vc_rsa".format(path)) + public = os.path.expanduser("{}/id_n2vc_rsa.pub".format(path)) + if os.path.exists(private) and os.path.exists(public): + return (private, public) + return (None, None) + + def find_juju_ssh_keys(): """Find the Juju ssh keys.""" @@ -444,6 +459,7 @@ class TestN2VC(object): """ debug("Running teardown_class...") try: + debug("Destroying LXD containers...") for application in self.state: if self.state[application]['container']: @@ -452,9 +468,10 @@ class TestN2VC(object): # Logout of N2VC if self.n2vc: - debug("Logging out of N2VC...") + debug("teardown_class(): Logging out of N2VC...") yield from self.n2vc.logout() - debug("Logging out of N2VC...done.") + debug("teardown_class(): Logging out of N2VC...done.") + debug("Running teardown_class...done.") except Exception as ex: debug("Exception in teardown_class: {}".format(ex)) @@ -573,20 +590,50 @@ class TestN2VC(object): if not self.n2vc: self.n2vc = get_n2vc(loop=loop) - vnf_name = self.n2vc.FormatApplicationName( + application = self.n2vc.FormatApplicationName( self.ns_name, self.vnf_name, str(vnf_index), ) + + # Initialize the state of the application + self.state[application] = { + 'status': None, # Juju status + 'container': None, # lxd container, for proxy charms + 'actions': {}, # Actions we've executed + 'done': False, # Are we done testing this charm? + 'phase': "deploy", # What phase is this application in? + } + debug("Deploying charm at {}".format(self.artifacts[charm])) + # If this is a native charm, we need to provision the underlying + # machine ala an LXC container. + machine_spec = {} + + if not self.isproxy(application): + debug("Creating container for native charm") + # args = ("default", application, None, None) + self.state[application]['container'] = create_lxd_container( + name=os.path.basename(__file__) + ) + + hostname = self.get_container_ip( + self.state[application]['container'], + ) + + machine_spec = { + 'host': hostname, + 'user': 'ubuntu', + } + await self.n2vc.DeployCharms( self.ns_name, - vnf_name, + application, self.vnfd, self.get_charm(charm), params, - {}, + machine_spec, self.n2vc_callback, ) @@ -722,6 +769,7 @@ class TestN2VC(object): @classmethod async def configure_proxy_charm(self, *args): + """Configure a container for use via ssh.""" (model, application, _, _) = args try: @@ -844,6 +892,17 @@ class TestN2VC(object): for application in self.charms: try: await self.n2vc.RemoveCharms(self.model, application) + + while True: + # Wait for the application to be removed + await asyncio.sleep(10) + if not await self.n2vc.HasApplication( + self.model, + application, + ): + break + + # Need to wait for the charm to finish, because native charms if self.state[application]['container']: debug("Deleting LXD container...") destroy_lxd_container( @@ -858,10 +917,10 @@ class TestN2VC(object): # Logout of N2VC try: - debug("Logging out of N2VC...") + debug("stop(): Logging out of N2VC...") await self.n2vc.logout() self.n2vc = None - debug("Logging out of N2VC...Done.") + debug("stop(): Logging out of N2VC...Done.") except Exception as ex: debug(ex) diff --git a/tests/test_ssh_keygen.py b/tests/test_ssh_keygen.py new file mode 100644 index 0000000..3a129a3 --- /dev/null +++ b/tests/test_ssh_keygen.py @@ -0,0 +1,18 @@ +""" +Test N2VC's ssh key generation +""" +import os +import pytest +from . import base +import tempfile + + +@pytest.mark.asyncio +async def test_ssh_keygen(monkeypatch): + with tempfile.TemporaryDirectory() as tmpdirname: + monkeypatch.setitem(os.environ, "HOME", tmpdirname) + + client = base.get_n2vc() + + public_key = await client.GetPublicKey() + assert len(public_key) -- 2.17.1