Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the launch command for launching an instance #59

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ rich # MIT
# Set upper bound to match Juju 3.1.x series target
juju<3.2 # Apache 2

# Used in the launch command to launch an instance
openstacksdk==0.61.*
petname

# Used for communication with snapd socket
requests # Apache 2
requests-unixsocket # Apache 2
Expand Down
30 changes: 28 additions & 2 deletions sunbeam/commands/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,12 @@ def _retrieve_admin_credentials(jhelper: JujuHelper, model: str) -> dict:
class UserOpenRCStep(BaseStep):
"""Generate openrc for created cloud user."""

def __init__(self, auth_url: str, auth_version: str, openrc: str):
def __init__(self, auth_url: str, auth_version: str, openrc: str, clouds: str):
super().__init__("Generate user openrc", "Generating openrc for cloud usage")
self.auth_url = auth_url
self.auth_version = auth_version
self.openrc = openrc
self.clouds = clouds

def is_skip(self, status: Optional["Status"] = None):
"""Determines if the step should be skipped or not.
Expand All @@ -253,6 +254,7 @@ def run(self, status: Optional[Status]) -> Result:
)
tf_output = json.loads(process.stdout)
self._print_openrc(tf_output)
self._print_clouds_yaml(tf_output)
return Result(ResultType.COMPLETED)
except subprocess.CalledProcessError as e:
LOG.exception("Error initializing Terraform")
Expand All @@ -279,6 +281,28 @@ def _print_openrc(self, tf_output: dict) -> None:
else:
console.print(_openrc)

def _print_clouds_yaml(self, tf_output: dict) -> None:
"""Print a clouds.yaml file and save to disk using provided information"""
_cloudsyaml = f"""
clouds:
sunbeam:
auth:
auth_url: {self.auth_url}
project_name: {tf_output["OS_PROJECT_NAME"]["value"]}
username: {tf_output["OS_USERNAME"]["value"]}
password: {tf_output["OS_PASSWORD"]["value"]}
user_domain_name: {tf_output["OS_USER_DOMAIN_NAME"]["value"]}
project_domain_name: {tf_output["OS_PROJECT_DOMAIN_NAME"]["value"]}
identity_api_version: {self.auth_version}
"""
if self.clouds:
message = f"Writing clouds.yaml to {self.clouds} ... "
console.status(message)
with open(self.clouds, "w") as f_clouds:
os.fchmod(f_clouds.fileno(), mode=0o640)
f_clouds.write(_cloudsyaml)
else:
console.print(_cloudsyaml)

class ConfigureCloudStep(BaseStep):
"""Default cloud configuration for all-in-one install."""
Expand Down Expand Up @@ -415,8 +439,9 @@ def run(self, status: Optional[Status]) -> Result:
@click.option("-a", "--accept-defaults", help="Accept all defaults.", is_flag=True)
@click.option("-p", "--preseed", help="Preseed file.")
@click.option("-o", "--openrc", help="Output file for cloud access details.")
@click.option("-c", "--clouds", help="Output file for clouds.yaml")
def configure(
openrc: str = None, preseed: str = None, accept_defaults: bool = False
openrc: str = None, preseed: str = None, accept_defaults: bool = False, clouds: str = None
) -> None:
"""Configure cloud with some sane defaults."""
snap = utils.get_snap()
Expand Down Expand Up @@ -452,6 +477,7 @@ def configure(
auth_url=admin_credentials["OS_AUTH_URL"],
auth_version=admin_credentials["OS_AUTH_VERSION"],
openrc=openrc,
clouds=clouds
),
UpdateExternalNetworkConfigStep(ext_network=ext_network_file),
]
Expand Down
132 changes: 132 additions & 0 deletions sunbeam/commands/launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Copyright (c) 2023 Canonical Ltd.
#
# 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 logging
import os
import subprocess

from typing import List

import click
import openstack
import petname


from rich.console import Console
from snaphelpers import Snap

LOG = logging.getLogger(__name__)
console = Console()
snap = Snap()


def check_output(*args: List[str]) -> str:
"""Execute a shell command, returning the output of the command.

:param args: strings to be composed into the bash call.

Include our env; pass in any extra keyword args.
"""
return subprocess.check_output(
args, universal_newlines=True, env=os.environ
).strip()


def check(*args: List[str]) -> int:
"""Execute a shell command, raising an error on failed excution.

:param args: strings to be composed into the bash call.

"""
return subprocess.check_call(args, env=os.environ)


def check_keypair(openstack_conn: openstack.connection.Connection):
"""
Check for the sunbeam keypair's existence, creating it if it doesn't.

"""
console.print("Checking for sunbeam key in OpenStack ... ")
home = os.environ.get("SNAP_REAL_HOME")
key_path = f"{home}/sunbeam"
try:
openstack_conn.compute.get_keypair("sunbeam")
console.print("Found sunbeam key!")
except openstack.exceptions.ResourceNotFound:
console.print(f"No sunbeam key found. Creating SSH key at {key_path}/sunbeam")
id_ = openstack_conn.compute.create_keypair(name="sunbeam")
with open(key_path, "w", encoding="utf-8") as file_:
file_.write(id_.private_key)
check("chmod", "600", key_path)
return key_path


@click.command()
@click.option(
"-k", "--key", default="sunbeam", help="The SSH key to use for the instance"
)
def launch(key: str = "sunbeam") -> None:
"""
Launch an OpenStack instance
"""
console.print("Launching an OpenStack instance ... ")
try:
conn = openstack.connect(cloud="sunbeam")
except openstack.exceptions.SDKException:
console.print(
"Unable to connect to OpenStack.",
" Is OpenStack running?",
" Have you run the configure command?",
" Do you have a clouds.yaml file?",
)
return

with console.status("Checking for SSH key pair ... "):
if key == "sunbeam":
# Make sure that we have a default ssh key to hand off to the
# instance.
key_path = check_keypair(conn)
else:
# We've been passed an ssh key with an unknown path. Drop in
# some placeholder text for the message at the end of this
# routine, but don't worry about verifying it. We trust the
# caller to have created it!
key_path = "/path/to/your/key"

with console.status("Creating the OpenStack instance ... "):
instance_name = petname.Generate()
image = conn.compute.find_image("ubuntu-jammy")
flavor = conn.compute.find_flavor("m1.tiny")
network = conn.network.find_network("demo-network")
keypair = conn.compute.find_keypair(key)
server = conn.compute.create_server(
name=instance_name,
image_id=image.id,
flavor_id=flavor.id,
networks=[{"uuid": network.id}],
key_name=keypair.name,
)

server = conn.compute.wait_for_server(server)
server_id = server.id

with console.status("Allocating IP address to instance ... "):
external_network = conn.network.find_network("external-network")
ip_ = conn.network.create_ip(floating_network_id=external_network.id)
conn.compute.add_floating_ip_to_server(server_id, ip_.floating_ip_address)

console.print(
"Access instance with", f"`ssh -i {key_path} ubuntu@{ip_.floating_ip_address}"
)
2 changes: 2 additions & 0 deletions sunbeam/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from sunbeam.commands import configure as configure_cmds
from sunbeam.commands import inspect as inspect_cmds
from sunbeam.commands import install_script as install_script_cmds
from sunbeam.commands import launch as launch_cmds
from sunbeam.commands import openrc as openrc_cmds
from sunbeam.commands import reset as reset_cmds
from sunbeam.commands import status as status_cmds
Expand Down Expand Up @@ -55,6 +56,7 @@ def main():
cli.add_command(configure_cmds.configure)
cli.add_command(inspect_cmds.inspect)
cli.add_command(install_script_cmds.install_script)
cli.add_command(launch_cmds.launch)
cli()


Expand Down