Skip to content

Commit 6657709

Browse files
authored
Merge pull request #7 from sandwichcloud/keypair
add keypair and network-port routes plus some other misc changes
2 parents 495f65c + 858463d commit 6657709

15 files changed

+243
-25
lines changed

deli_counter/http/mounts/root/mount.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ def __setup_tools(self):
9999
cherrypy.tools.authentication = cherrypy.Tool('on_start_resource', self.validate_token, priority=20)
100100
cherrypy.tools.project_scope = cherrypy.Tool('on_start_resource', self.validate_project_scope, priority=30)
101101

102-
cherrypy.tools.resource_object = cherrypy.Tool('before_request_body', self.resource_object, priority=40)
103-
cherrypy.tools.enforce_policy = cherrypy.Tool('before_request_body', self.enforce_policy, priority=50)
102+
cherrypy.tools.enforce_policy = cherrypy.Tool('before_request_body', self.enforce_policy, priority=40)
103+
cherrypy.tools.resource_object = cherrypy.Tool('before_request_body', self.resource_object, priority=50)
104104

105105
def __setup_auth(self):
106106
self.auth_manager = AuthManager()

deli_counter/http/mounts/root/routes/v1/images.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ def delete(self, image_id):
117117
raise cherrypy.HTTPError(409, "Cannot delete an image while it is locked.")
118118

119119
image.state = ImageState.DELETING
120-
create_task(session, image, delete_image, image_id=image.id)
120+
# TODO: do we allow not deleting image backing?
121+
create_task(session, image, delete_image, image_id=image.id, delete_backing=True)
121122

122123
session.commit()
123124

deli_counter/http/mounts/root/routes/v1/instance.py

+12-10
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
from ingredients_db.models.authn import AuthNServiceAccount
1111
from ingredients_db.models.images import Image, ImageVisibility, ImageState
1212
from ingredients_db.models.instance import Instance, InstanceState
13+
from ingredients_db.models.keypair import Keypair
1314
from ingredients_db.models.network import Network, NetworkState
1415
from ingredients_db.models.network_port import NetworkPort
15-
from ingredients_db.models.public_key import PublicKey
1616
from ingredients_db.models.region import Region, RegionState
1717
from ingredients_db.models.task import Task
1818
from ingredients_db.models.zones import Zone, ZoneState
@@ -115,6 +115,7 @@ def create(self):
115115

116116
network_port = NetworkPort()
117117
network_port.network_id = network.id
118+
network_port.project_id = project.id
118119
session.add(network_port)
119120
session.flush()
120121

@@ -133,16 +134,15 @@ def create(self):
133134
session.add(instance)
134135
session.flush()
135136

136-
for public_key_id in request.public_keys:
137-
public_key = session.query(PublicKey).filter(PublicKey.id == public_key_id).filter(
138-
PublicKey.project_id == project.id).first()
137+
for keypair_id in request.keypair_ids:
138+
keypair = session.query(Keypair).filter(Keypair.id == keypair_id).filter(
139+
Keypair.project_id == project.id).first()
139140

140-
if public_key is None:
141-
raise cherrypy.HTTPError(404,
142-
"Could not find a public key within the scoped project with the id %s" %
143-
public_key_id)
141+
if keypair is None:
142+
raise cherrypy.HTTPError(404, "Could not find a keypair within the scoped project with the id %s" %
143+
keypair_id)
144144

145-
instance.public_keys.append(public_key)
145+
instance.keypairs.append(keypair)
146146

147147
print("TASK")
148148
create_task(session, instance, create_instance, instance_id=instance.id)
@@ -159,7 +159,9 @@ def create(self):
159159
@cherrypy.tools.resource_object(id_param="instance_id", cls=Instance)
160160
@cherrypy.tools.enforce_policy(policy_name="instances:get")
161161
def get(self, instance_id: uuid.UUID):
162-
return ResponseInstance.from_database(cherrypy.request.resource_object)
162+
with cherrypy.request.db_session() as session:
163+
instance: Instance = session.merge(cherrypy.request.resource_object, load=False)
164+
return ResponseInstance.from_database(instance)
163165

164166
@Route()
165167
@cherrypy.tools.project_scope()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import uuid
2+
3+
import cherrypy
4+
from sqlalchemy.orm import Query
5+
6+
from deli_counter.http.mounts.root.routes.v1.validation_models.keypairs import RequestCreateKeypair, \
7+
ParamsListKeypair, ParamsKeypair, ResponseKeypair
8+
from ingredients_db.models.keypair import Keypair
9+
from ingredients_http.request_methods import RequestMethods
10+
from ingredients_http.route import Route
11+
from ingredients_http.router import Router
12+
13+
14+
class KeypairsRouter(Router):
15+
def __init__(self):
16+
super().__init__(uri_base='keypairs')
17+
18+
@Route(methods=[RequestMethods.POST])
19+
@cherrypy.tools.project_scope()
20+
@cherrypy.tools.model_in(cls=RequestCreateKeypair)
21+
@cherrypy.tools.model_out(cls=ResponseKeypair)
22+
@cherrypy.tools.enforce_policy(policy_name="keypairs:create")
23+
def create(self):
24+
request: RequestCreateKeypair = cherrypy.request.model
25+
project = cherrypy.request.project
26+
27+
with cherrypy.request.db_session() as session:
28+
keypair = session.query(Keypair).filter(Keypair.name == request.name).first()
29+
30+
if keypair is not None:
31+
raise cherrypy.HTTPError(409, "A keypair with the requested name already exists.")
32+
33+
keypair = Keypair()
34+
keypair.name = request.name
35+
keypair.public_key = request.public_key
36+
keypair.project_id = project.id
37+
session.add(keypair)
38+
session.commit()
39+
session.refresh(keypair)
40+
return ResponseKeypair.from_database(keypair)
41+
42+
@Route(route='{keypair_id}')
43+
@cherrypy.tools.project_scope()
44+
@cherrypy.tools.model_params(cls=ParamsKeypair)
45+
@cherrypy.tools.model_out(cls=ResponseKeypair)
46+
@cherrypy.tools.resource_object(id_param="keypair_id", cls=Keypair)
47+
@cherrypy.tools.enforce_policy(policy_name="keypairs:get")
48+
def get(self, keypair_id: uuid.UUID):
49+
return ResponseKeypair.from_database(cherrypy.request.resource_object)
50+
51+
@Route()
52+
@cherrypy.tools.project_scope()
53+
@cherrypy.tools.model_params(cls=ParamsListKeypair)
54+
@cherrypy.tools.model_out_pagination(cls=ResponseKeypair)
55+
@cherrypy.tools.enforce_policy(policy_name="keypairs:list")
56+
def list(self, limit: int, marker: uuid.UUID):
57+
project = cherrypy.request.project
58+
starting_query = Query(Keypair).filter(Keypair.project_id == project.id)
59+
return self.paginate(Keypair, ResponseKeypair, limit, marker, starting_query=starting_query)
60+
61+
@Route(route='{keypair_id}', methods=[RequestMethods.DELETE])
62+
@cherrypy.tools.project_scope()
63+
@cherrypy.tools.model_params(cls=ParamsKeypair)
64+
@cherrypy.tools.resource_object(id_param="keypair_id", cls=Keypair)
65+
@cherrypy.tools.enforce_policy(policy_name="keypairs:delete")
66+
def delete(self, keypair_id: uuid.UUID):
67+
cherrypy.response.status = 204
68+
# Fix for https://github.com/cherrypy/cherrypy/issues/1657
69+
del cherrypy.response.headers['Content-Type']
70+
71+
with cherrypy.request.db_session() as session:
72+
keypair: Keypair = session.merge(cherrypy.request.resource_object, load=False)
73+
session.delete(keypair)
74+
session.commit()
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,58 @@
1+
import uuid
2+
3+
import cherrypy
4+
from sqlalchemy.orm import Query
5+
6+
from deli_counter.http.mounts.root.routes.v1.validation_models.network_ports import ParamsNetworkPort, \
7+
ParamsListNetworkPort, ResponseNetworkPort
8+
from ingredients_db.models.instance import Instance
9+
from ingredients_db.models.network_port import NetworkPort
10+
from ingredients_http.request_methods import RequestMethods
11+
from ingredients_http.route import Route
112
from ingredients_http.router import Router
213

314

415
class NetworkPortRouter(Router):
516
def __init__(self):
617
super().__init__(uri_base='network-ports')
718

8-
# TODO: do this
19+
# Do we want to allow creation? It's kinda pointless since they are auto created
20+
21+
@Route(route='{network_port_id}')
22+
@cherrypy.tools.project_scope()
23+
@cherrypy.tools.model_params(cls=ParamsNetworkPort)
24+
@cherrypy.tools.model_out(cls=ResponseNetworkPort)
25+
@cherrypy.tools.resource_object(id_param="network_port_id", cls=NetworkPort)
26+
@cherrypy.tools.enforce_policy(policy_name="network_ports:get")
27+
def get(self, network_port_id):
28+
return ResponseNetworkPort.from_database(cherrypy.request.resource_object)
29+
30+
@Route()
31+
@cherrypy.tools.project_scope()
32+
@cherrypy.tools.model_params(cls=ParamsListNetworkPort)
33+
@cherrypy.tools.model_out_pagination(cls=ResponseNetworkPort)
34+
@cherrypy.tools.enforce_policy(policy_name="network_ports:list")
35+
def list(self, limit: int, marker: uuid.UUID):
36+
project = cherrypy.request.project
37+
starting_query = Query(NetworkPort).filter(NetworkPort.project_id == project.id)
38+
return self.paginate(NetworkPort, ResponseNetworkPort, limit, marker, starting_query=starting_query)
39+
40+
@Route(route='{network_port_id}', methods=[RequestMethods.DELETE])
41+
@cherrypy.tools.project_scope()
42+
@cherrypy.tools.model_params(cls=ParamsNetworkPort)
43+
@cherrypy.tools.resource_object(id_param="network_port_id", cls=NetworkPort)
44+
@cherrypy.tools.enforce_policy(policy_name="network_ports:delete")
45+
def delete(self, network_port_id):
46+
cherrypy.response.status = 204
47+
# Fix for https://github.com/cherrypy/cherrypy/issues/1657
48+
del cherrypy.response.headers['Content-Type']
49+
50+
with cherrypy.request.db_session() as session:
51+
network_port: NetworkPort = session.merge(cherrypy.request.resource_object, load=False)
52+
53+
instance_count = session.query(Instance).filter(Instance.network_port_id == network_port.id).count()
54+
if instance_count > 0:
55+
raise cherrypy.HTTPError(400, "Cannot delete a network port while it is in use.")
56+
57+
session.delete(network_port)
58+
session.commit()

deli_counter/http/mounts/root/routes/v1/networks.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,16 @@ def get(self, network_id):
8181
@cherrypy.tools.model_params(cls=ParamsListNetwork)
8282
@cherrypy.tools.model_out_pagination(cls=ResponseNetwork)
8383
@cherrypy.tools.enforce_policy(policy_name="networks:list")
84-
def list(self, region_id, limit: int, marker: uuid.UUID):
84+
def list(self, name, region_id, limit: int, marker: uuid.UUID):
8585
starting_query = Query(Network)
8686
if region_id is not None:
8787
with cherrypy.request.db_session() as session:
8888
region = session.query(Region).filter(Region.id == region_id).first()
8989
if region is None:
9090
raise cherrypy.HTTPError(404, "A region with the requested id does not exist.")
9191
starting_query = starting_query.filter(Network.region_id == region.id)
92+
if name is not None:
93+
starting_query = starting_query.filter(Network.name == name)
9294
return self.paginate(Network, ResponseNetwork, limit, marker, starting_query=starting_query)
9395

9496
@Route(route='{network_id}', methods=[RequestMethods.DELETE])

deli_counter/http/mounts/root/routes/v1/regions.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import cherrypy
2+
from sqlalchemy.orm import Query
23

34
from deli_counter.http.mounts.root.routes.v1.validation_models.regions import ResponseRegion, RequestCreateRegion, \
45
ParamsRegion, ParamsListRegion, RequestRegionSchedule
@@ -58,8 +59,11 @@ def get(self, region_id):
5859
@cherrypy.tools.model_params(cls=ParamsListRegion)
5960
@cherrypy.tools.model_out_pagination(cls=ResponseRegion)
6061
@cherrypy.tools.enforce_policy(policy_name="regions:list")
61-
def list(self, limit, marker):
62-
return self.paginate(Region, ResponseRegion, limit, marker)
62+
def list(self, name, limit, marker):
63+
starting_query = None
64+
if name is not None:
65+
starting_query = Query(Region).filter(Region.name == name)
66+
return self.paginate(Region, ResponseRegion, limit, marker, starting_query=starting_query)
6367

6468
@Route(route='{region_id}', methods=[RequestMethods.DELETE])
6569
@cherrypy.tools.model_params(cls=ParamsRegion)

deli_counter/http/mounts/root/routes/v1/validation_models/instances.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class RequestCreateInstance(Model):
1313
network_id = UUIDType(required=True)
1414
region_id = UUIDType(required=True)
1515
zone_id = UUIDType()
16-
public_keys = ListType(UUIDType, default=list)
16+
keypair_ids = ListType(UUIDType, default=list)
1717
tags = DictType(StringType)
1818

1919

@@ -24,7 +24,8 @@ class ResponseInstance(Model):
2424
network_port_id = UUIDType(required=True)
2525
region_id = UUIDType(required=True)
2626
zone_id = UUIDType()
27-
public_keys = ListType(UUIDType, default=list)
27+
service_account_id = UUIDType()
28+
keypair_ids = ListType(UUIDType, default=list)
2829
state = EnumType(InstanceState, required=True)
2930
tags = DictType(StringType, default=dict)
3031

@@ -40,11 +41,15 @@ def from_database(cls, instance: Instance):
4041
instance_model.network_port_id = instance.network_port_id
4142
instance_model.state = instance.state
4243
instance_model.tags = instance.tags
44+
instance_model.service_account_id = instance.service_account_id
4345

4446
instance_model.region_id = instance.region_id
4547
if instance.zone_id is not None:
4648
instance_model.zone_id = instance.zone_id
4749

50+
for keypair in instance.keypairs:
51+
instance_model.keypair_ids.append(keypair.id)
52+
4853
instance_model.created_at = instance.created_at
4954
instance_model.updated_at = instance.updated_at
5055

@@ -70,8 +75,7 @@ class RequestInstanceImage(Model):
7075

7176
class RequestInstancePowerOffRestart(Model):
7277
hard = BooleanType(default=False)
73-
timeout = IntType(default=60, min_value=60,
74-
max_value=300) # If your vm takes more than 5 minutes to power off you are doing something bad
78+
timeout = IntType(default=60, min_value=60, max_value=300)
7579

7680

7781
class RequestInstanceResetState(Model):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from cryptography.exceptions import UnsupportedAlgorithm
2+
from cryptography.hazmat.backends import default_backend
3+
from cryptography.hazmat.primitives.serialization import load_ssh_public_key
4+
from schematics import Model
5+
from schematics.exceptions import ValidationError
6+
from schematics.types import UUIDType, IntType, StringType
7+
8+
from ingredients_db.models.keypair import Keypair
9+
10+
11+
class ParamsKeypair(Model):
12+
keypair_id = UUIDType(required=True)
13+
14+
15+
class ParamsListKeypair(Model):
16+
limit = IntType(default=100, max_value=100, min_value=1)
17+
marker = UUIDType()
18+
19+
20+
class RequestCreateKeypair(Model):
21+
name = StringType(required=True, min_length=3)
22+
public_key = StringType(required=True)
23+
24+
def validate_public_key(self, data, value):
25+
try:
26+
load_ssh_public_key(value.encode(), default_backend())
27+
except ValueError:
28+
raise ValidationError("public_key could not be decoded or is not in the proper format")
29+
except UnsupportedAlgorithm:
30+
raise ValidationError("public_key serialization type is not supported")
31+
32+
return value
33+
34+
35+
class ResponseKeypair(Model):
36+
id = UUIDType(required=True)
37+
name = StringType(required=True, min_length=3)
38+
public_key = StringType(required=True)
39+
40+
@classmethod
41+
def from_database(cls, keypair: Keypair):
42+
model = cls()
43+
model.id = keypair.id
44+
model.name = keypair.name
45+
model.public_key = keypair.public_key
46+
47+
return model
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from schematics import Model
2+
from schematics.types import UUIDType, IntType
3+
4+
from ingredients_db.models.network_port import NetworkPort
5+
from ingredients_http.schematics.types import IPv4AddressType
6+
7+
8+
class ParamsNetworkPort(Model):
9+
network_port_id = UUIDType(required=True)
10+
11+
12+
class ParamsListNetworkPort(Model):
13+
limit = IntType(default=100, max_value=100, min_value=1)
14+
marker = UUIDType()
15+
16+
17+
class ResponseNetworkPort(Model):
18+
id = UUIDType(required=True)
19+
network_id = UUIDType(required=True)
20+
ip_address = IPv4AddressType(required=True)
21+
22+
@classmethod
23+
def from_database(cls, network_port: NetworkPort):
24+
model = cls()
25+
model.id = network_port.id
26+
model.network_id = network_port.network_id
27+
model.ip_address = network_port.ip_address
28+
29+
return model

deli_counter/http/mounts/root/routes/v1/validation_models/networks.py

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class ParamsNetwork(Model):
9090

9191

9292
class ParamsListNetwork(Model):
93+
name = StringType()
9394
region_id = UUIDType()
9495
limit = IntType(default=100, max_value=100, min_value=1)
9596
marker = UUIDType()

deli_counter/http/mounts/root/routes/v1/validation_models/regions.py

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ParamsRegion(Model):
1717

1818

1919
class ParamsListRegion(Model):
20+
name = StringType()
2021
limit = IntType(default=100, max_value=100, min_value=1)
2122
marker = UUIDType()
2223

deli_counter/test/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ def create_instance(self, app, project=None, region=None, zone=None, image=None,
262262

263263
network_port = NetworkPort()
264264
network_port.network_id = network.id
265+
network_port.project_id = project.id
265266
network_port.ip_address = network.next_free_address(session)
266267

267268
session.add(network_port)

0 commit comments

Comments
 (0)