Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions etc/genesis_universal_agent/genesis_universal_agent.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ connection_pool_size = 2
[universal_agent]
orch_endpoint = http://localhost:11011
status_endpoint = http://localhost:11012
caps_drivers = CoreCapabilityDriver
caps_drivers = CoreCapabilityDriver,PasswordCapabilityDriver


[universal_agent_scheduler]
capabilities = em_core_*
capabilities = em_core_*,password


[CoreCapabilityDriver]
Expand All @@ -25,3 +25,4 @@ user_api_base_url = http://localhost:11010
project_id = 12345678-c625-4fee-81d5-f691897b8142
em_core_compute_nodes = /v1/nodes/
em_core_config_configs = /v1/config/configs/
em_core_secret_passwords = /v1/secret/passwords/
Empty file.
Empty file.
121 changes: 121 additions & 0 deletions genesis_core/agent/universal/drivers/secret/backend/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Copyright 2025 Genesis Corporation.
#
# 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.
from __future__ import annotations

import secrets
import logging
import typing as tp

from restalchemy.dm import filters as dm_filters
from restalchemy.storage import exceptions as ra_exc
from gcl_sdk.agents.universal.dm import models
from gcl_sdk.agents.universal.clients.backend import base
from gcl_sdk.agents.universal.clients.backend import exceptions

from genesis_core.secret import constants as sc
from genesis_core.secret.dm import models as secret_dm
from genesis_core.agent.universal.drivers.secret.dm import models as driver_dm


LOG = logging.getLogger(__name__)


class DatabaseSecretBackendClient(base.AbstractBackendClient):
"""Secret Backend client based on SQL database."""

def get(self, resource: models.Resource) -> dict[str, tp.Any]:
"""Get the resource value in dictionary format."""
try:
driver_password = driver_dm.Password.objects.get_one(
filters={
"uuid": dm_filters.EQ(resource.uuid),
},
)
except ra_exc.RecordNotFound:
raise exceptions.ResourceNotFound(resource=resource)

return driver_password.meta

def create(self, resource: models.Resource) -> dict[str, tp.Any]:
"""Creates the resource. Returns the created resource."""
try:
self.get(resource)
except exceptions.ResourceNotFound:
pass
else:
raise exceptions.ResourceAlreadyExists(resource=resource)

password = secret_dm.Password.from_ua_resource(resource)

# Validate structure of password model
if (
sc.SecretMethod[password.method].is_auto
and password.value is not None
):
raise ValueError("Cannot create auto-generated password.")

if (
not sc.SecretMethod[password.method].is_auto
and password.value is None
):
raise ValueError("Cannot create non-auto-generated password.")

# Generate plain password
if sc.SecretMethod[password.method].is_auto:
if password.method == sc.SecretMethod.AUTO_HEX:
plain_password = secrets.token_hex(16)
elif password.method == sc.SecretMethod.AUTO_URL_SAFE:
plain_password = secrets.token_urlsafe(16)
else:
raise ValueError("Unknown auto-generated password method.")
else:
plain_password = password.value

# Build password from the plain view
pass_value = password.constructor.build(plain_password)

# Build storagable password and save
driver_password = driver_dm.Password.from_password_resource(
resource, pass_value
)
driver_password.save()
return driver_password.meta

def update(self, resource: models.Resource) -> dict[str, tp.Any]:
"""Update the resource. Returns the updated resource."""

# The simplest implementation. Update via recreation.
self.delete(resource)
return self.create(resource)

def list(self, kind: str, **kwargs) -> list[dict[str, tp.Any]]:
"""Lists all resources by kind."""
secrets = driver_dm.Password.objects.get_all()
Comment thread
phantomii marked this conversation as resolved.
return [s.meta for s in secrets]

def delete(self, resource: models.Resource) -> None:
"""Delete the resource."""
try:
self.get(resource)
except exceptions.ResourceNotFound:
raise exceptions.ResourceNotFound(resource=resource)

password = driver_dm.Password.objects.get_one(
filters={
"uuid": dm_filters.EQ(resource.uuid),
}
)
password.delete()
Empty file.
61 changes: 61 additions & 0 deletions genesis_core/agent/universal/drivers/secret/dm/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2025 Genesis Corporation.
#
# 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.
from __future__ import annotations

import typing as tp

from restalchemy.dm import properties
from restalchemy.dm import models
from restalchemy.dm import types
from restalchemy.storage.sql import orm
from gcl_sdk.agents.universal.dm import models as ua_models

from genesis_core.secret import constants as sc
from genesis_core.secret.dm import models as secret_dm


class Password(
models.ModelWithUUID,
models.ModelWithTimestamp,
orm.SQLStorableMixin,
):
__tablename__ = "storage_passwords"

status = properties.property(
types.Enum([s.value for s in sc.SecretStatus]),
default=sc.SecretStatus.NEW.value,
)
value = properties.property(
types.String(min_length=1, max_length=512),
required=True,
)
# Some additional metadata about the secret
meta = properties.property(types.Dict(), default=lambda: {})

@classmethod
def from_password_resource(
cls, resource: ua_models.TargetResource, password_value: str
) -> Password:
meta = resource.value.copy()
meta["value"] = password_value
meta["status"] = sc.SecretStatus.ACTIVE.value

return cls(
uuid=resource.uuid,
value=password_value,
status=sc.SecretStatus.ACTIVE.value,
meta=meta,
)
44 changes: 44 additions & 0 deletions genesis_core/agent/universal/drivers/secret/password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2025 Genesis Corporation.
#
# 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.
from __future__ import annotations

import logging

from gcl_sdk.agents.universal.drivers import direct
from gcl_sdk.agents.universal.storage import fs

from genesis_core.agent.universal.drivers.secret.backend import db as db_back


LOG = logging.getLogger(__name__)

AGENT_WORK_DIR = "/var/lib/genesis/universal_agent/"


class PasswordCapabilityDriver(direct.DirectAgentDriver):
"""Password capability driver."""

def __init__(self):
storage = fs.FileAgentStorage(
AGENT_WORK_DIR, "password_cap_storage.json"
)
client = db_back.DatabaseSecretBackendClient()

super().__init__(storage=storage, client=client)

def get_capabilities(self) -> list[str]:
"""Returns a list of capabilities supported by the driver."""
return ["password"]
2 changes: 2 additions & 0 deletions genesis_core/config/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import typing as tp

DEFAULT_SQL_LIMIT = 100
CONFIG_KIND = "config"
RENDER_KIND = "render"


class ConfigStatus(str, enum.Enum):
Expand Down
72 changes: 7 additions & 65 deletions genesis_core/config/dm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@
import uuid as sys_uuid

from restalchemy.dm import models
from restalchemy.dm import filters as dm_filters
from restalchemy.dm import properties
from restalchemy.dm import types
from restalchemy.dm import types_dynamic
from restalchemy.storage.sql import orm
from restalchemy.storage.sql import engines

from gcl_sdk.agents.universal.dm import models as ua_models

Expand Down Expand Up @@ -135,6 +133,7 @@ class Config(
cm.ModelWithFullAsset,
orm.SQLStorableMixin,
ua_models.TargetResourceMixin,
ua_models.TargetResourceSQLStorableMixin,
):
__tablename__ = "config_configs"

Expand Down Expand Up @@ -205,86 +204,29 @@ def render(self, node: sys_uuid.UUID) -> ua_models.TargetResource:
)

resource = render.to_ua_resource("render", master=self.uuid)
resource.calculate_hash()
resource.status = cc.ConfigStatus.IN_PROGRESS.value
return resource

@classmethod
def get_new_configs(
cls, limit: int = cc.DEFAULT_SQL_LIMIT
) -> list["Config"]:
expression = (
"SELECT "
" config_configs.uuid as uuid "
"FROM config_configs LEFT JOIN ua_target_resources ON "
" config_configs.uuid = ua_target_resources.uuid "
"WHERE ua_target_resources.uuid is NULL "
"LIMIT %s;"
)
params = (limit,)

engine = engines.engine_factory.get_engine()
with engine.session_manager() as session:
curs = session.execute(expression, params)
response = curs.fetchall()

if not response:
return []

return cls.objects.get_all(
filters={"uuid": dm_filters.In(str(r["uuid"]) for r in response)},
)
return cls.get_new_entities(cls.__tablename__, cc.CONFIG_KIND, limit)

@classmethod
def get_updated_configs(
cls, limit: int = cc.DEFAULT_SQL_LIMIT
) -> list["Config"]:
expression = (
"SELECT "
" config_configs.uuid as uuid "
"FROM config_configs INNER JOIN ua_target_resources ON "
" config_configs.uuid = ua_target_resources.uuid "
"WHERE config_configs.updated_at != ua_target_resources.tracked_at "
"LIMIT %s;"
)
params = (limit,)

engine = engines.engine_factory.get_engine()
with engine.session_manager() as session:
curs = session.execute(expression, params)
response = curs.fetchall()

if not response:
return []

return cls.objects.get_all(
filters={"uuid": dm_filters.In(str(r["uuid"]) for r in response)},
return cls.get_updated_entities(
cls.__tablename__, cc.CONFIG_KIND, limit
)

@classmethod
def get_deleted_config_renders(
cls, limit: int = cc.DEFAULT_SQL_LIMIT
) -> list[ua_models.TargetResource]:
expression = (
"SELECT "
" ua_target_resources.uuid as uuid "
"FROM ua_target_resources LEFT JOIN config_configs ON "
" ua_target_resources.uuid = config_configs.uuid "
"WHERE ua_target_resources.kind = 'config' "
" AND config_configs.uuid is NULL "
"LIMIT %s;"
)
params = (limit,)

engine = engines.engine_factory.get_engine()
with engine.session_manager() as session:
curs = session.execute(expression, params)
response = curs.fetchall()

if not response:
return []

return ua_models.TargetResource.objects.get_all(
filters={"uuid": dm_filters.In(str(r["uuid"]) for r in response)},
return cls.get_deleted_target_resources(
cls.__tablename__, cc.CONFIG_KIND, limit
)


Expand Down
Loading