Compare commits

...

4 Commits

334 changed files with 102058 additions and 0 deletions

View File

@@ -0,0 +1,308 @@
================
Cetmix Tower AWS
================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:e427fa4368b78aacbc6faa6232b54496e83fbe4905ddef4cc17f3913e5ee3ee0
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github
:target: https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_aws
:alt: cetmix/cetmix-tower
|badge1| |badge2| |badge3|
**⚠️ DISCLAIMER: TECHNICAL MODULE**
This module integrates Boto3 library, the Amazon Web Services (AWS)
Software Development Kit (SDK) for Python, into the Cetmix Tower.
**This is a technical module intended for system administrators or
DevOps professionals.** It may involve server configuration,
infrastructure management, or advanced setup outside the standard
Odoo interface. Not intended for direct use by end users.
**Table of contents**
.. contents::
:local:
Use Cases / Context
===================
Although Amazon Web Services (AWS) allows API calls without using an
SDK, we found that integrating the Amazon SDK into Cetmix Tower makes
provisioning, configuring, and maintaining AWS instances more convenient
for the end user. However, not all Cetmix Tower users require this
functionality, so to avoid overloading the system, we have included it
in a separate module.
Configuration
=============
**Prerequisites**
The module has ``boto3`` defined in its external dependencies, which
means, you should install the Python ``boto3`` package manually if you
don't have automatic package installation configured in your Odoo
environment. Run ``pip install boto3`` to install it.
**Setting up AWS Access**
1. **Create AWS Access Keys**
To use the AWS integration with Cetmix Tower, you need to create AWS
access keys:
- Follow the `official AWS
documentation <https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html>`__
(https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html)
for creating IAM access keys
- It's recommended to create a dedicated IAM user with appropriate
permissions for Cetmix Tower
- Store your access key ID and secret access key securely - you'll
need them in the next step
2. **Configure AWS Secrets in Cetmix Tower**
Create two secrets in Cetmix Tower to store your AWS credentials:
- Navigate to ``Cetmix Tower > Settings > Keys and Secrets``
- Create a new Secret with:
- Name: ``AWS Access Key``
- Reference: ``aws_access_key``
- Key Type: ``Secret``
- Enter your AWS access key ID in the Secret Value tab
- Similarly, create another Secret with:
- Name: ``AWS Secret Access Key``
- Reference: ``aws_secret_access_key``
- Key Type: ``Secret``
- Enter your AWS secret access key in the Secret Value tab
..
Note: These secrets will be accessible as
``#!cxtower.secret.aws_access_key!#`` and
``#!cxtower.secret.aws_secret_access_key!#`` in your commands.
3. **Configure AWS Region**
Create a variable to define your AWS region:
- Navigate to ``Cetmix Tower > Settings > Variables``
- Create a new Variable with:
- Name: ``AWS Region Name``
- Reference: ``aws_region_name``
- Type: ``String``
- Set your AWS region (e.g., ``us-east-1``, ``eu-west-1``) as the
value
Usage
=====
Please check the `official Boto3
Documentation <https://boto3.amazonaws.com/v1/documentation/api/latest/index.html>`__
(https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) for
the detailed information about the services and methods provided by the
Boto3 library.
**Disclaimer**: The following example demonstrates one of many
possible commands you can create and run with this module. The boto3
library provides access to the full range of AWS services and methods
- this is just a starting point to help you get familiar with the
integration.
Example of Cetmix Tower Python Command to List EC2 Instances
------------------------------------------------------------
Navigate to Command Creation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Go to ``Cetmix Tower > Commands > Commands``
- Click the ``Create`` button
Configure Command Settings
~~~~~~~~~~~~~~~~~~~~~~~~~~
- Set a descriptive ``Name`` (e.g., "List AWS EC2 Instances")
- Leave ``Reference`` blank to generate automatically (or set a custom
reference)
- Select ``Action``: "Execute Python code"
- Set ``Access Level``: Choose appropriate level (e.g., "Manager")
- Optional: Set ``Default Path`` if needed
- Optional: Add ``Tags`` (e.g., "aws", "ec2") for better organization
Add Required Variables
~~~~~~~~~~~~~~~~~~~~~~
- In the ``Variables`` tab, add the previously configured variable:
- ``aws_region_name``
Add Required Secrets
~~~~~~~~~~~~~~~~~~~~
- In the ``Secrets`` field, add the previously configured secrets:
- ``aws_access_key``
- ``aws_secret_access_key``
Write Python Code
~~~~~~~~~~~~~~~~~
- Go to the ``Code`` tab
- Enter the following Python code:
.. code:: python
# List EC2 instances using boto3
result = {"exit_code": 0, "message": None}
session = boto3.Session(
aws_access_key_id=#!cxtower.secret.aws_access_key!#,
aws_secret_access_key=#!cxtower.secret.aws_secret_access_key!#,
region_name={{ aws_region_name }}
)
ec2 = session.client('ec2')
instances = ec2.describe_instances()
instance_details = []
for reservation in instances['Reservations']:
for instance in reservation['Instances']:
instance_detail = "Instance ID: " + instance['InstanceId']
instance_detail += ", Type: " + instance.get('InstanceType', 'Unknown')
instance_detail += ", State: " + instance.get('State', {}).get('Name', 'Unknown')
instance_details.append(instance_detail)
if instance_details:
result["message"] = "Found " + str(len(instance_details)) + " EC2 instances:\n" + "\n".join(instance_details)
else:
result["message"] = "No EC2 instances found"
Save the Command
~~~~~~~~~~~~~~~~
- Click the ``Save`` button to create the command
Running the AWS EC2 Command
---------------------------
Navigate to Server
~~~~~~~~~~~~~~~~~~
- Go to ``Cetmix Tower > Servers > Servers``
- Open the server where you want to run the command
Execute Command from Server
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Click the ``Command`` button at the top of the server form
- In the popup dialog:
- Select your AWS EC2 command from the dropdown
- Verify the variable values (if any need adjustment)
- Click ``Run`` to execute
View Command Results
~~~~~~~~~~~~~~~~~~~~
- After execution, the command log will display showing:
- The command executed
- Execution status
- Output message containing EC2 instance details if successful
Example Output
--------------
For a successful execution with EC2 instances:
.. code:: bash
Found 3 EC2 instances:
Instance ID: i-0abc123def456789, Type: t2.micro, State: running
Instance ID: i-0def456abc789123, Type: t3.medium, State: stopped
Instance ID: i-0789abc123def456, Type: m5.large, State: running
For a successful execution with no EC2 instances:
.. code:: bash
No EC2 instances found
Creating Additional AWS Commands
--------------------------------
The cetmix_tower_aws module provides access to the boto3 Python library
for AWS service integration. Here are some common services you can use:
.. code:: python
# Standard client initialization pattern
client = boto3.client(
'service_name', # Replace with: ec2, s3, rds, cloudwatch, etc.
region_name={{ aws_region_name }},
aws_access_key_id=#!cxtower.secret.aws_access_key!#,
aws_secret_access_key=#!cxtower.secret.aws_secret_access_key!#
)
# Or use resource interface for object-oriented access
resource = boto3.resource(
'service_name', # Replace with: ec2, s3, etc.
region_name={{ aws_region_name }},
aws_access_key_id=#!cxtower.secret.aws_access_key!#,
aws_secret_access_key=#!cxtower.secret.aws_secret_access_key!#
)
Popular AWS services include: EC2 (compute), S3 (storage), RDS
(databases), and CloudWatch (monitoring).
For more details, see the `AWS Boto3
Documentation <https://boto3.amazonaws.com/v1/documentation/api/latest/index.html>`__.
Changelog
=========
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/cetmix/cetmix-tower/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_aws%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Cetmix
Maintainers
-----------
This module is part of the `cetmix/cetmix-tower <https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_aws>`_ project on GitHub.
You are welcome to contribute.

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,24 @@
# Copyright 2024 Cetmix OÜ
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Cetmix Tower AWS",
"summary": """Cetmix Tower AWS EC2 API integration""",
"version": "18.0.1.0.1",
"category": "Productivity",
"license": "AGPL-3",
"author": "Cetmix",
"website": "https://tower.cetmix.com",
"live_test_url": "https://tower.cetmix.com/download",
"images": ["static/description/banner.png"],
"installable": True,
"external_dependencies": {
"python": ["boto3"],
},
"depends": [
"cetmix_tower_server",
],
"demo": [
"demo/demo_data.xml",
],
}

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<!-- ===============================================
Keys
================================================ -->
<record id="aws_access_key" model="cx.tower.key">
<field name="name">AWS Access Key</field>
<field name="reference">aws_access_key</field>
<field name="key_type">s</field>
<field name="secret_value">AKIAIOSFODNN7EXAMPLE</field>
</record>
<record id="aws_secret_access_key" model="cx.tower.key">
<field name="name">AWS Secret Access Key</field>
<field name="reference">aws_secret_access_key</field>
<field name="key_type">s</field>
<field name="secret_value">wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY</field>
</record>
<!-- ===============================================
Variables
================================================ -->
<record id="variable_aws_region_name" model="cx.tower.variable">
<field name="name">AWS Region Name</field>
<field name="reference">aws_region_name</field>
</record>
<!-- ===============================================
Global Variable values
================================================ -->
<record id="global_value_aws_region" model="cx.tower.variable.value">
<field name="variable_id" ref="variable_aws_region_name" />
<field name="value_char">us-east-1</field>
</record>
<!-- ===============================================
Commands
================================================ -->
<record id="command_list_ec2_instances" model="cx.tower.command">
<field name="name">List AWS EC2 Instances</field>
<field name="action">python_code</field>
<field name="code">
# List EC2 instances using boto3
result = {"exit_code": 0, "message": None}
session = boto3.Session(
aws_access_key_id=#!cxtower.secret.aws_access_key!#,
aws_secret_access_key=#!cxtower.secret.aws_secret_access_key!#,
region_name={{ aws_region_name }}
)
ec2 = session.client('ec2')
instances = ec2.describe_instances()
instance_details = []
for reservation in instances['Reservations']:
for instance in reservation['Instances']:
instance_detail = "Instance ID: " + instance['InstanceId']
instance_detail += ", Type: " + instance.get('InstanceType', 'Unknown')
instance_detail += ", State: " + instance.get('State', {}).get('Name', 'Unknown')
instance_details.append(instance_detail)
if instance_details:
result["message"] = "Found " + str(len(instance_details)) + " EC2 instances:\n" + "\n".join(instance_details)
else:
result["message"] = "No EC2 instances found"
</field>
<field name="access_level">1</field>
<field
name="tag_ids"
eval="[(6, 0, [ref('cetmix_tower_server.tag_custom')])]"
/>
<field name="note">List EC2 instances using boto3 AWS SDK</field>
</record>
</odoo>

View File

@@ -0,0 +1,31 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * cetmix_tower_aws
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: cetmix_tower_aws
#: model:ir.model,name:cetmix_tower_aws.model_cx_tower_command
msgid "Cetmix Tower Command"
msgstr ""
#. module: cetmix_tower_aws
#. odoo-python
#: code:addons/cetmix_tower_aws/models/cx_tower_command.py:0
msgid ""
"Python 'boto3' library for AWS services. Available methods: 'client', "
"'resource', 'Session'<br/>Supports AWS services like EC2, S3, RDS, Lambda, "
"CloudWatch, etc.<br/>Please check the <a "
"href='https://boto3.amazonaws.com/v1/documentation/api/latest/index.html' "
"target='_blank'>Boto3 Documentation</a> for the detailed information about "
"the services and methods."
msgstr ""

View File

@@ -0,0 +1,39 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * cetmix_tower_aws
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.3\n"
"Language: it\n"
#. module: cetmix_tower_aws
#: model:ir.model,name:cetmix_tower_aws.model_cx_tower_command
msgid "Cetmix Tower Command"
msgstr "Comando Cetmix Tower"
#. module: cetmix_tower_aws
#. odoo-python
#: code:addons/cetmix_tower_aws/models/cx_tower_command.py:0
#, python-format
msgid "Python 'boto3' library for AWS services. Available methods: 'client', 'resource', 'Session'<br/>Supports AWS services like EC2, S3, RDS, Lambda, CloudWatch, etc.<br/>Please check the <a href='https://boto3.amazonaws.com/v1/documentation/api/latest/index.html' target='_blank'>Boto3 Documentation</a> for the detailed information about the services and methods."
msgstr "Libreria Python 'boto3' per servizi AWS. Metodi disponibili: 'client', 'resource', 'Session'<br/>Supporta servizi AWS come EC2, S3, RDS, Lambda, CloudWatch, ecc.<br/>Consultare la <a href='https://boto3.amazonaws.com/v1/documentation/api/latest/index.html' target='_blank'>documentazione Boto3</a> per informazioni dettagliate su servizi e metodi."
#~ msgid ""
#~ "# - boto3: Python 'boto3' library for AWS services. Available methods: "
#~ "'client', 'resource', 'Session'\n"
#~ "# Supports AWS services like EC2, S3, RDS, Lambda, CloudWatch, etc."
#~ msgstr ""
#~ "# - boto3: libreria Python 'boto3' per servizi AWS. Metodi disponibili: "
#~ "'client', 'resource', 'Session'\n"
#~ "# Supporta servizi AWS come EC2, S3, RDS, Lambda, CloudWatch, ecc."

View File

@@ -0,0 +1 @@
from . import cx_tower_command

View File

@@ -0,0 +1,40 @@
# Copyright 2024 Cetmix OÜ
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, models
from odoo.tools.safe_eval import wrap_module
# Wrap boto3 safely
boto3 = wrap_module(__import__("boto3"), ["client", "resource", "Session"])
class CxTowerCommand(models.Model):
"""Extends cx.tower.command to add AWS boto3 functionality."""
_inherit = "cx.tower.command"
def _custom_python_libraries(self):
"""
Add the boto3 library to the available libraries.
"""
custom_python_libraries = super()._custom_python_libraries()
custom_python_libraries.update(
{
"cetmix_tower_aws": {
"boto3": {
"import": boto3,
"help": _(
"Python 'boto3' library for AWS services. "
"Available methods: 'client', 'resource', 'Session'<br/>"
"Supports AWS services like EC2, S3, RDS, Lambda, "
"CloudWatch, etc.<br/>"
"Please check the <a "
"href='https://boto3.amazonaws.com/v1/documentation/api/latest/index.html'"
" target='_blank'>Boto3 Documentation</a> for the detailed "
"information about the services and methods."
),
},
}
}
)
return custom_python_libraries

View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1,42 @@
**Prerequisites**
The module has `boto3` defined in its external dependencies, which means, you should install the Python `boto3` package manually if you don't have automatic package installation configured in your Odoo environment. Run `pip install boto3` to install it.
**Setting up AWS Access**
1. **Create AWS Access Keys**
To use the AWS integration with Cetmix Tower, you need to create AWS access keys:
- Follow the [official AWS documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html) (https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html) for creating IAM access keys
- It's recommended to create a dedicated IAM user with appropriate permissions for Cetmix Tower
- Store your access key ID and secret access key securely - you'll need them in the next step
2. **Configure AWS Secrets in Cetmix Tower**
Create two secrets in Cetmix Tower to store your AWS credentials:
- Navigate to `Cetmix Tower > Settings > Keys and Secrets`
- Create a new Secret with:
- Name: `AWS Access Key`
- Reference: `aws_access_key`
- Key Type: `Secret`
- Enter your AWS access key ID in the Secret Value tab
- Similarly, create another Secret with:
- Name: `AWS Secret Access Key`
- Reference: `aws_secret_access_key`
- Key Type: `Secret`
- Enter your AWS secret access key in the Secret Value tab
> Note: These secrets will be accessible as `#!cxtower.secret.aws_access_key!#` and `#!cxtower.secret.aws_secret_access_key!#` in your commands.
3. **Configure AWS Region**
Create a variable to define your AWS region:
- Navigate to `Cetmix Tower > Settings > Variables`
- Create a new Variable with:
- Name: `AWS Region Name`
- Reference: `aws_region_name`
- Type: `String`
- Set your AWS region (e.g., `us-east-1`, `eu-west-1`) as the value

View File

@@ -0,0 +1,5 @@
Although Amazon Web Services (AWS) allows API calls without using an SDK, we found that
integrating the Amazon SDK into Cetmix Tower makes provisioning, configuring, and
maintaining AWS instances more convenient for the end user. However, not all Cetmix
Tower users require this functionality, so to avoid overloading the system, we have
included it in a separate module.

View File

@@ -0,0 +1,6 @@
**⚠️ DISCLAIMER: TECHNICAL MODULE**
This module integrates Boto3 library, the Amazon Web Services (AWS) Software Development Kit (SDK) for Python, into the Cetmix Tower.
> **This is a technical module intended for system administrators or DevOps professionals.** It may involve server configuration, infrastructure management, or advanced setup outside the standard Odoo interface. Not intended for direct use by end users.

View File

@@ -0,0 +1,121 @@
Please check the [official Boto3 Documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) (https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) for the detailed information about the services and methods provided by the Boto3 library.
> **Disclaimer**: The following example demonstrates one of many possible commands you can create and run with this module. The boto3 library provides access to the full range of AWS services and methods - this is just a starting point to help you get familiar with the integration.
## Example of Cetmix Tower Python Command to List EC2 Instances
### Navigate to Command Creation
- Go to `Cetmix Tower > Commands > Commands`
- Click the `Create` button
### Configure Command Settings
- Set a descriptive `Name` (e.g., "List AWS EC2 Instances")
- Leave `Reference` blank to generate automatically (or set a custom reference)
- Select `Action`: "Execute Python code"
- Set `Access Level`: Choose appropriate level (e.g., "Manager")
- Optional: Set `Default Path` if needed
- Optional: Add `Tags` (e.g., "aws", "ec2") for better organization
### Add Required Variables
- In the `Variables` tab, add the previously configured variable:
- `aws_region_name`
### Add Required Secrets
- In the `Secrets` field, add the previously configured secrets:
- `aws_access_key`
- `aws_secret_access_key`
### Write Python Code
- Go to the `Code` tab
- Enter the following Python code:
```python
# List EC2 instances using boto3
result = {"exit_code": 0, "message": None}
session = boto3.Session(
aws_access_key_id=#!cxtower.secret.aws_access_key!#,
aws_secret_access_key=#!cxtower.secret.aws_secret_access_key!#,
region_name={{ aws_region_name }}
)
ec2 = session.client('ec2')
instances = ec2.describe_instances()
instance_details = []
for reservation in instances['Reservations']:
for instance in reservation['Instances']:
instance_detail = "Instance ID: " + instance['InstanceId']
instance_detail += ", Type: " + instance.get('InstanceType', 'Unknown')
instance_detail += ", State: " + instance.get('State', {}).get('Name', 'Unknown')
instance_details.append(instance_detail)
if instance_details:
result["message"] = "Found " + str(len(instance_details)) + " EC2 instances:\n" + "\n".join(instance_details)
else:
result["message"] = "No EC2 instances found"
```
### Save the Command
- Click the `Save` button to create the command
## Running the AWS EC2 Command
### Navigate to Server
- Go to `Cetmix Tower > Servers > Servers`
- Open the server where you want to run the command
### Execute Command from Server
- Click the `Command` button at the top of the server form
- In the popup dialog:
- Select your AWS EC2 command from the dropdown
- Verify the variable values (if any need adjustment)
- Click `Run` to execute
### View Command Results
- After execution, the command log will display showing:
- The command executed
- Execution status
- Output message containing EC2 instance details if successful
## Example Output
For a successful execution with EC2 instances:
```bash
Found 3 EC2 instances:
Instance ID: i-0abc123def456789, Type: t2.micro, State: running
Instance ID: i-0def456abc789123, Type: t3.medium, State: stopped
Instance ID: i-0789abc123def456, Type: m5.large, State: running
```
For a successful execution with no EC2 instances:
```bash
No EC2 instances found
```
## Creating Additional AWS Commands
The cetmix_tower_aws module provides access to the boto3 Python library for AWS service integration. Here are some common services you can use:
```python
# Standard client initialization pattern
client = boto3.client(
'service_name', # Replace with: ec2, s3, rds, cloudwatch, etc.
region_name={{ aws_region_name }},
aws_access_key_id=#!cxtower.secret.aws_access_key!#,
aws_secret_access_key=#!cxtower.secret.aws_secret_access_key!#
)
# Or use resource interface for object-oriented access
resource = boto3.resource(
'service_name', # Replace with: ec2, s3, etc.
region_name={{ aws_region_name }},
aws_access_key_id=#!cxtower.secret.aws_access_key!#,
aws_secret_access_key=#!cxtower.secret.aws_secret_access_key!#
)
```
Popular AWS services include: EC2 (compute), S3 (storage), RDS (databases), and CloudWatch (monitoring).
For more details, see the [AWS Boto3 Documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html).

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,684 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Cetmix Tower AWS</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="cetmix-tower-aws">
<h1 class="title">Cetmix Tower AWS</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:e427fa4368b78aacbc6faa6232b54496e83fbe4905ddef4cc17f3913e5ee3ee0
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_aws"><img alt="cetmix/cetmix-tower" src="https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github" /></a></p>
<p><strong>⚠️ DISCLAIMER: TECHNICAL MODULE</strong></p>
<p>This module integrates Boto3 library, the Amazon Web Services (AWS)
Software Development Kit (SDK) for Python, into the Cetmix Tower.</p>
<blockquote>
<strong>This is a technical module intended for system administrators or
DevOps professionals.</strong> It may involve server configuration,
infrastructure management, or advanced setup outside the standard
Odoo interface. Not intended for direct use by end users.</blockquote>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#use-cases-context" id="toc-entry-1">Use Cases / Context</a></li>
<li><a class="reference internal" href="#configuration" id="toc-entry-2">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-3">Usage</a><ul>
<li><a class="reference internal" href="#example-of-cetmix-tower-python-command-to-list-ec2-instances" id="toc-entry-4">Example of Cetmix Tower Python Command to List EC2 Instances</a><ul>
<li><a class="reference internal" href="#navigate-to-command-creation" id="toc-entry-5">Navigate to Command Creation</a></li>
<li><a class="reference internal" href="#configure-command-settings" id="toc-entry-6">Configure Command Settings</a></li>
<li><a class="reference internal" href="#add-required-variables" id="toc-entry-7">Add Required Variables</a></li>
<li><a class="reference internal" href="#add-required-secrets" id="toc-entry-8">Add Required Secrets</a></li>
<li><a class="reference internal" href="#write-python-code" id="toc-entry-9">Write Python Code</a></li>
<li><a class="reference internal" href="#save-the-command" id="toc-entry-10">Save the Command</a></li>
</ul>
</li>
<li><a class="reference internal" href="#running-the-aws-ec2-command" id="toc-entry-11">Running the AWS EC2 Command</a><ul>
<li><a class="reference internal" href="#navigate-to-server" id="toc-entry-12">Navigate to Server</a></li>
<li><a class="reference internal" href="#execute-command-from-server" id="toc-entry-13">Execute Command from Server</a></li>
<li><a class="reference internal" href="#view-command-results" id="toc-entry-14">View Command Results</a></li>
</ul>
</li>
<li><a class="reference internal" href="#example-output" id="toc-entry-15">Example Output</a></li>
<li><a class="reference internal" href="#creating-additional-aws-commands" id="toc-entry-16">Creating Additional AWS Commands</a></li>
</ul>
</li>
<li><a class="reference internal" href="#changelog" id="toc-entry-17">Changelog</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-18">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-19">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-20">Authors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-21">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="use-cases-context">
<h1><a class="toc-backref" href="#toc-entry-1">Use Cases / Context</a></h1>
<p>Although Amazon Web Services (AWS) allows API calls without using an
SDK, we found that integrating the Amazon SDK into Cetmix Tower makes
provisioning, configuring, and maintaining AWS instances more convenient
for the end user. However, not all Cetmix Tower users require this
functionality, so to avoid overloading the system, we have included it
in a separate module.</p>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-2">Configuration</a></h1>
<p><strong>Prerequisites</strong></p>
<p>The module has <tt class="docutils literal">boto3</tt> defined in its external dependencies, which
means, you should install the Python <tt class="docutils literal">boto3</tt> package manually if you
dont have automatic package installation configured in your Odoo
environment. Run <tt class="docutils literal">pip install boto3</tt> to install it.</p>
<p><strong>Setting up AWS Access</strong></p>
<ol class="arabic">
<li><p class="first"><strong>Create AWS Access Keys</strong></p>
<p>To use the AWS integration with Cetmix Tower, you need to create AWS
access keys:</p>
<ul class="simple">
<li>Follow the <a class="reference external" href="https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html">official AWS
documentation</a>
(<a class="reference external" href="https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html">https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html</a>)
for creating IAM access keys</li>
<li>Its recommended to create a dedicated IAM user with appropriate
permissions for Cetmix Tower</li>
<li>Store your access key ID and secret access key securely - youll
need them in the next step</li>
</ul>
</li>
<li><p class="first"><strong>Configure AWS Secrets in Cetmix Tower</strong></p>
<p>Create two secrets in Cetmix Tower to store your AWS credentials:</p>
<ul class="simple">
<li>Navigate to <tt class="docutils literal">Cetmix Tower &gt; Settings &gt; Keys and Secrets</tt></li>
<li>Create a new Secret with:<ul>
<li>Name: <tt class="docutils literal">AWS Access Key</tt></li>
<li>Reference: <tt class="docutils literal">aws_access_key</tt></li>
<li>Key Type: <tt class="docutils literal">Secret</tt></li>
</ul>
</li>
<li>Enter your AWS access key ID in the Secret Value tab</li>
<li>Similarly, create another Secret with:<ul>
<li>Name: <tt class="docutils literal">AWS Secret Access Key</tt></li>
<li>Reference: <tt class="docutils literal">aws_secret_access_key</tt></li>
<li>Key Type: <tt class="docutils literal">Secret</tt></li>
</ul>
</li>
<li>Enter your AWS secret access key in the Secret Value tab</li>
</ul>
<!-- -->
<blockquote>
<p>Note: These secrets will be accessible as
<tt class="docutils literal">#!cxtower.secret.aws_access_key!#</tt> and
<tt class="docutils literal">#!cxtower.secret.aws_secret_access_key!#</tt> in your commands.</p>
</blockquote>
</li>
<li><p class="first"><strong>Configure AWS Region</strong></p>
<p>Create a variable to define your AWS region:</p>
<ul class="simple">
<li>Navigate to <tt class="docutils literal">Cetmix Tower &gt; Settings &gt; Variables</tt></li>
<li>Create a new Variable with:<ul>
<li>Name: <tt class="docutils literal">AWS Region Name</tt></li>
<li>Reference: <tt class="docutils literal">aws_region_name</tt></li>
<li>Type: <tt class="docutils literal">String</tt></li>
</ul>
</li>
<li>Set your AWS region (e.g., <tt class="docutils literal"><span class="pre">us-east-1</span></tt>, <tt class="docutils literal"><span class="pre">eu-west-1</span></tt>) as the
value</li>
</ul>
</li>
</ol>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-3">Usage</a></h1>
<p>Please check the <a class="reference external" href="https://boto3.amazonaws.com/v1/documentation/api/latest/index.html">official Boto3
Documentation</a>
(<a class="reference external" href="https://boto3.amazonaws.com/v1/documentation/api/latest/index.html">https://boto3.amazonaws.com/v1/documentation/api/latest/index.html</a>) for
the detailed information about the services and methods provided by the
Boto3 library.</p>
<blockquote>
<strong>Disclaimer</strong>: The following example demonstrates one of many
possible commands you can create and run with this module. The boto3
library provides access to the full range of AWS services and methods
- this is just a starting point to help you get familiar with the
integration.</blockquote>
<div class="section" id="example-of-cetmix-tower-python-command-to-list-ec2-instances">
<h2><a class="toc-backref" href="#toc-entry-4">Example of Cetmix Tower Python Command to List EC2 Instances</a></h2>
<div class="section" id="navigate-to-command-creation">
<h3><a class="toc-backref" href="#toc-entry-5">Navigate to Command Creation</a></h3>
<ul class="simple">
<li>Go to <tt class="docutils literal">Cetmix Tower &gt; Commands &gt; Commands</tt></li>
<li>Click the <tt class="docutils literal">Create</tt> button</li>
</ul>
</div>
<div class="section" id="configure-command-settings">
<h3><a class="toc-backref" href="#toc-entry-6">Configure Command Settings</a></h3>
<ul class="simple">
<li>Set a descriptive <tt class="docutils literal">Name</tt> (e.g., “List AWS EC2 Instances”)</li>
<li>Leave <tt class="docutils literal">Reference</tt> blank to generate automatically (or set a custom
reference)</li>
<li>Select <tt class="docutils literal">Action</tt>: “Execute Python code”</li>
<li>Set <tt class="docutils literal">Access Level</tt>: Choose appropriate level (e.g., “Manager”)</li>
<li>Optional: Set <tt class="docutils literal">Default Path</tt> if needed</li>
<li>Optional: Add <tt class="docutils literal">Tags</tt> (e.g., “aws”, “ec2”) for better organization</li>
</ul>
</div>
<div class="section" id="add-required-variables">
<h3><a class="toc-backref" href="#toc-entry-7">Add Required Variables</a></h3>
<ul class="simple">
<li>In the <tt class="docutils literal">Variables</tt> tab, add the previously configured variable:<ul>
<li><tt class="docutils literal">aws_region_name</tt></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="add-required-secrets">
<h3><a class="toc-backref" href="#toc-entry-8">Add Required Secrets</a></h3>
<ul class="simple">
<li>In the <tt class="docutils literal">Secrets</tt> field, add the previously configured secrets:<ul>
<li><tt class="docutils literal">aws_access_key</tt></li>
<li><tt class="docutils literal">aws_secret_access_key</tt></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="write-python-code">
<h3><a class="toc-backref" href="#toc-entry-9">Write Python Code</a></h3>
<ul>
<li><p class="first">Go to the <tt class="docutils literal">Code</tt> tab</p>
</li>
<li><p class="first">Enter the following Python code:</p>
<pre class="code python literal-block">
<span class="c1"># List EC2 instances using boto3</span><span class="w">
</span><span class="n">result</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&quot;exit_code&quot;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s2">&quot;message&quot;</span><span class="p">:</span> <span class="kc">None</span><span class="p">}</span><span class="w">
</span><span class="n">session</span> <span class="o">=</span> <span class="n">boto3</span><span class="o">.</span><span class="n">Session</span><span class="p">(</span><span class="w">
</span> <span class="n">aws_access_key_id</span><span class="o">=</span><span class="c1">#!cxtower.secret.aws_access_key!#,</span><span class="w">
</span> <span class="n">aws_secret_access_key</span><span class="o">=</span><span class="c1">#!cxtower.secret.aws_secret_access_key!#,</span><span class="w">
</span> <span class="n">region_name</span><span class="o">=</span><span class="p">{{</span> <span class="n">aws_region_name</span> <span class="p">}}</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="n">ec2</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">client</span><span class="p">(</span><span class="s1">'ec2'</span><span class="p">)</span><span class="w">
</span><span class="n">instances</span> <span class="o">=</span> <span class="n">ec2</span><span class="o">.</span><span class="n">describe_instances</span><span class="p">()</span><span class="w">
</span><span class="n">instance_details</span> <span class="o">=</span> <span class="p">[]</span><span class="w">
</span><span class="k">for</span> <span class="n">reservation</span> <span class="ow">in</span> <span class="n">instances</span><span class="p">[</span><span class="s1">'Reservations'</span><span class="p">]:</span><span class="w">
</span> <span class="k">for</span> <span class="n">instance</span> <span class="ow">in</span> <span class="n">reservation</span><span class="p">[</span><span class="s1">'Instances'</span><span class="p">]:</span><span class="w">
</span> <span class="n">instance_detail</span> <span class="o">=</span> <span class="s2">&quot;Instance ID: &quot;</span> <span class="o">+</span> <span class="n">instance</span><span class="p">[</span><span class="s1">'InstanceId'</span><span class="p">]</span><span class="w">
</span> <span class="n">instance_detail</span> <span class="o">+=</span> <span class="s2">&quot;, Type: &quot;</span> <span class="o">+</span> <span class="n">instance</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'InstanceType'</span><span class="p">,</span> <span class="s1">'Unknown'</span><span class="p">)</span><span class="w">
</span> <span class="n">instance_detail</span> <span class="o">+=</span> <span class="s2">&quot;, State: &quot;</span> <span class="o">+</span> <span class="n">instance</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'State'</span><span class="p">,</span> <span class="p">{})</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'Name'</span><span class="p">,</span> <span class="s1">'Unknown'</span><span class="p">)</span><span class="w">
</span> <span class="n">instance_details</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">instance_detail</span><span class="p">)</span><span class="w">
</span><span class="k">if</span> <span class="n">instance_details</span><span class="p">:</span><span class="w">
</span> <span class="n">result</span><span class="p">[</span><span class="s2">&quot;message&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&quot;Found &quot;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">instance_details</span><span class="p">))</span> <span class="o">+</span> <span class="s2">&quot; EC2 instances:</span><span class="se">\n</span><span class="s2">&quot;</span> <span class="o">+</span> <span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">instance_details</span><span class="p">)</span><span class="w">
</span><span class="k">else</span><span class="p">:</span><span class="w">
</span> <span class="n">result</span><span class="p">[</span><span class="s2">&quot;message&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&quot;No EC2 instances found&quot;</span>
</pre>
</li>
</ul>
</div>
<div class="section" id="save-the-command">
<h3><a class="toc-backref" href="#toc-entry-10">Save the Command</a></h3>
<ul class="simple">
<li>Click the <tt class="docutils literal">Save</tt> button to create the command</li>
</ul>
</div>
</div>
<div class="section" id="running-the-aws-ec2-command">
<h2><a class="toc-backref" href="#toc-entry-11">Running the AWS EC2 Command</a></h2>
<div class="section" id="navigate-to-server">
<h3><a class="toc-backref" href="#toc-entry-12">Navigate to Server</a></h3>
<ul class="simple">
<li>Go to <tt class="docutils literal">Cetmix Tower &gt; Servers &gt; Servers</tt></li>
<li>Open the server where you want to run the command</li>
</ul>
</div>
<div class="section" id="execute-command-from-server">
<h3><a class="toc-backref" href="#toc-entry-13">Execute Command from Server</a></h3>
<ul class="simple">
<li>Click the <tt class="docutils literal">Command</tt> button at the top of the server form</li>
<li>In the popup dialog:<ul>
<li>Select your AWS EC2 command from the dropdown</li>
<li>Verify the variable values (if any need adjustment)</li>
<li>Click <tt class="docutils literal">Run</tt> to execute</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="view-command-results">
<h3><a class="toc-backref" href="#toc-entry-14">View Command Results</a></h3>
<ul class="simple">
<li>After execution, the command log will display showing:<ul>
<li>The command executed</li>
<li>Execution status</li>
<li>Output message containing EC2 instance details if successful</li>
</ul>
</li>
</ul>
</div>
</div>
<div class="section" id="example-output">
<h2><a class="toc-backref" href="#toc-entry-15">Example Output</a></h2>
<p>For a successful execution with EC2 instances:</p>
<pre class="code bash literal-block">
Found<span class="w"> </span><span class="m">3</span><span class="w"> </span>EC2<span class="w"> </span>instances:<span class="w">
</span>Instance<span class="w"> </span>ID:<span class="w"> </span>i-0abc123def456789,<span class="w"> </span>Type:<span class="w"> </span>t2.micro,<span class="w"> </span>State:<span class="w"> </span>running<span class="w">
</span>Instance<span class="w"> </span>ID:<span class="w"> </span>i-0def456abc789123,<span class="w"> </span>Type:<span class="w"> </span>t3.medium,<span class="w"> </span>State:<span class="w"> </span>stopped<span class="w">
</span>Instance<span class="w"> </span>ID:<span class="w"> </span>i-0789abc123def456,<span class="w"> </span>Type:<span class="w"> </span>m5.large,<span class="w"> </span>State:<span class="w"> </span>running
</pre>
<p>For a successful execution with no EC2 instances:</p>
<pre class="code bash literal-block">
No<span class="w"> </span>EC2<span class="w"> </span>instances<span class="w"> </span>found
</pre>
</div>
<div class="section" id="creating-additional-aws-commands">
<h2><a class="toc-backref" href="#toc-entry-16">Creating Additional AWS Commands</a></h2>
<p>The cetmix_tower_aws module provides access to the boto3 Python library
for AWS service integration. Here are some common services you can use:</p>
<pre class="code python literal-block">
<span class="c1"># Standard client initialization pattern</span><span class="w">
</span><span class="n">client</span> <span class="o">=</span> <span class="n">boto3</span><span class="o">.</span><span class="n">client</span><span class="p">(</span><span class="w">
</span> <span class="s1">'service_name'</span><span class="p">,</span> <span class="c1"># Replace with: ec2, s3, rds, cloudwatch, etc.</span><span class="w">
</span> <span class="n">region_name</span><span class="o">=</span><span class="p">{{</span> <span class="n">aws_region_name</span> <span class="p">}},</span><span class="w">
</span> <span class="n">aws_access_key_id</span><span class="o">=</span><span class="c1">#!cxtower.secret.aws_access_key!#,</span><span class="w">
</span> <span class="n">aws_secret_access_key</span><span class="o">=</span><span class="c1">#!cxtower.secret.aws_secret_access_key!#</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c1"># Or use resource interface for object-oriented access</span><span class="w">
</span><span class="n">resource</span> <span class="o">=</span> <span class="n">boto3</span><span class="o">.</span><span class="n">resource</span><span class="p">(</span><span class="w">
</span> <span class="s1">'service_name'</span><span class="p">,</span> <span class="c1"># Replace with: ec2, s3, etc.</span><span class="w">
</span> <span class="n">region_name</span><span class="o">=</span><span class="p">{{</span> <span class="n">aws_region_name</span> <span class="p">}},</span><span class="w">
</span> <span class="n">aws_access_key_id</span><span class="o">=</span><span class="c1">#!cxtower.secret.aws_access_key!#,</span><span class="w">
</span> <span class="n">aws_secret_access_key</span><span class="o">=</span><span class="c1">#!cxtower.secret.aws_secret_access_key!#</span><span class="w">
</span><span class="p">)</span>
</pre>
<p>Popular AWS services include: EC2 (compute), S3 (storage), RDS
(databases), and CloudWatch (monitoring).</p>
<p>For more details, see the <a class="reference external" href="https://boto3.amazonaws.com/v1/documentation/api/latest/index.html">AWS Boto3
Documentation</a>.</p>
</div>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-17">Changelog</a></h1>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-18">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_aws%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-19">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-20">Authors</a></h2>
<ul class="simple">
<li>Cetmix</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-21">Maintainers</a></h2>
<p>This module is part of the <a class="reference external" href="https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_aws">cetmix/cetmix-tower</a> project on GitHub.</p>
<p>You are welcome to contribute.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1 @@
from . import test_boto3_integration

View File

@@ -0,0 +1,64 @@
# Copyright 2024 Cetmix OÜ
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.tests import common
class TestBoto3Integration(common.TransactionCase):
"""Test boto3 integration with Cetmix Tower commands."""
def setUp(self):
super().setUp()
# Create a test command
self.command = self.env["cx.tower.command"].create(
{
"name": "Test AWS Command",
"action": "python_code",
}
)
self.eval_context = self.env[
"cx.tower.command"
]._get_python_command_eval_context()
def test_boto3_in_evaluation_context(self):
"""Test that boto3 is added to the evaluation context."""
# Get evaluation context
# Check if boto3 is in the evaluation context
self.assertIn("boto3", self.eval_context)
# Check available methods
boto3_obj = self.eval_context["boto3"]
self.assertTrue(hasattr(boto3_obj, "client"))
self.assertTrue(hasattr(boto3_obj, "resource"))
self.assertTrue(hasattr(boto3_obj, "Session"))
def test_boto3_in_evaluation_context_with_server(self):
"""Test that boto3 is added to the evaluation context
when server is provided."""
# Create a test server
test_server = self.env["cx.tower.server"].create(
{
"name": "Test AWS Server",
"reference": "test_aws_server",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"host_key": "test_key",
}
)
# Get evaluation context with server
eval_context = self.env["cx.tower.command"]._get_python_command_eval_context(
server=test_server
)
# Check if boto3 is in the evaluation context
self.assertIn("boto3", eval_context)
# Check available methods
boto3_obj = eval_context["boto3"]
self.assertTrue(hasattr(boto3_obj, "client"))
self.assertTrue(hasattr(boto3_obj, "resource"))
self.assertTrue(hasattr(boto3_obj, "Session"))
# Verify server is correctly passed to the context
self.assertEqual(eval_context["server"], test_server)

View File

@@ -0,0 +1,91 @@
================
Cetmix Tower Git
================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:2a3dd557b4ae104188b8e2912c02b135bcfe7cc6518f8633daa149ccb94850fe
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github
:target: https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_git
:alt: cetmix/cetmix-tower
|badge1| |badge2| |badge3|
This module implements Git Management functionality for `Cetmix
Tower <https://cetmix.com/tower>`__.
Please refer to the `official
documentation <https://cetmix.com/tower>`__ for detailed information.
**Table of contents**
.. contents::
:local:
Configuration
=============
Please refer to the `official
documentation <https://cetmix.com/tower>`__ for detailed configuration
instructions.
Usage
=====
Please refer to the `official
documentation <https://cetmix.com/tower>`__ for detailed usage
instructions.
Changelog
=========
18.0.1.0.2 (2026-03-10)
-----------------------
- Features: Provide git project name using the ``__git_project__``
custom value when creating a project in flight plan. Improve the UI
and UX of Git Projects. (5197)
- Bugfixes: Link server to git project only once. (5214)
18.0.1.0.1 (2025-12-17)
-----------------------
- Features: Improve search views, implement the search panel for
selected views. (5139)
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/cetmix/cetmix-tower/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_git%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Cetmix
Maintainers
-----------
This module is part of the `cetmix/cetmix-tower <https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_git>`_ project on GitHub.
You are welcome to contribute.

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,40 @@
# Copyright Cetmix OU
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Cetmix Tower Git",
"summary": "Cetmix Tower Git Management Tools",
"version": "18.0.1.0.2",
"development_status": "Beta",
"category": "Productivity",
"website": "https://tower.cetmix.com",
"author": "Cetmix",
"license": "AGPL-3",
"application": False,
"depends": ["cetmix_tower_yaml"],
"external_dependencies": {
"python": ["giturlparse==0.12.0"],
},
"data": [
"security/ir.model.access.csv",
"security/cx_tower_git_project_security.xml",
"security/cx_tower_git_source_security.xml",
"security/cx_tower_git_remote_security.xml",
"security/cx_tower_git_repo_security.xml",
"security/cx_tower_git_repo_owner_security.xml",
"security/cx_tower_git_project_rel_security.xml",
"security/cx_tower_git_project_file_template_rel_security.xml",
"views/cx_tower_git_project_views.xml",
"views/cx_tower_git_source_views.xml",
"views/cx_tower_git_remote_views.xml",
"views/cx_tower_git_repo_views.xml",
"views/cx_tower_git_repo_owner_views.xml",
"views/cx_tower_file_views.xml",
"views/cx_tower_file_template_views.xml",
"views/cx_tower_server_view.xml",
"views/cx_tower_plan_line_view.xml",
"views/menuitems.xml",
],
"demo": [
"demo/demo_data.xml",
],
}

View File

@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<!-- Git Project -->
<record id="git_project_demo" model="cx.tower.git.project">
<field name="name">Demo Git Project</field>
<field name="reference">demo_git_project</field>
<field name="note">This is a demo git project.</field>
</record>
<!-- Repositories -->
<record id="repo_demo_cetmix_tower" model="cx.tower.git.repo">
<field name="url">https://github.com/cetmix-demo/cetmix-tower-demo.git</field>
</record>
<record id="repo_demo_oca_web" model="cx.tower.git.repo">
<field name="url">https://github.com/oca-demo/web-demo.git</field>
</record>
<record id="repo_demo_odoo_enterprise" model="cx.tower.git.repo">
<field name="url">https://github.com/odoo-demo/enterprise-demo.git</field>
<field name="is_private" eval="True" />
</record>
<record id="repo_demo_gitlab_private" model="cx.tower.git.repo">
<field name="url">https://gitlab.com/cetmix-demo/cetmix-tower-demo.git</field>
<field name="is_private" eval="True" />
</record>
<record id="repo_demo_bitbucket_private" model="cx.tower.git.repo">
<field
name="url"
>https://bitbucket.com/cetmix-demo/cetmix-tower-demo-enterprise.git</field>
<field name="is_private" eval="True" />
</record>
<!-- Sources -->
<!-- Cetmix Tower -->
<record id="source_demo_cetmix_tower" model="cx.tower.git.source">
<field name="name">Cetmix Tower</field>
<field name="reference">cetmix_tower</field>
<field name="git_project_id" ref="git_project_demo" />
</record>
<!-- Remotes-->
<record id="remote_demo_cetmix_tower_14_0_dev" model="cx.tower.git.remote">
<field name="source_id" ref="source_demo_cetmix_tower" />
<field name="repo_id" ref="repo_demo_cetmix_tower" />
<field name="head_type">branch</field>
<field name="head">14.0</field>
</record>
<record id="remote_demo_cetmix_tower_pr_176" model="cx.tower.git.remote">
<field name="source_id" ref="source_demo_cetmix_tower" />
<field name="repo_id" ref="repo_demo_cetmix_tower" />
<field name="head_type">pr</field>
<field name="head">176</field>
</record>
<!-- OCA Web -->
<record id="source_demo_oca_web" model="cx.tower.git.source">
<field name="name">OCA Web</field>
<field name="reference">oca_web</field>
<field name="git_project_id" ref="git_project_demo" />
</record>
<!-- Remotes -->
<record id="remote_demo_oca_web_14_0" model="cx.tower.git.remote">
<field name="source_id" ref="source_demo_oca_web" />
<field name="repo_id" ref="repo_demo_oca_web" />
<field name="head_type">branch</field>
<field name="head">14.0</field>
</record>
<!-- Odoo Enterprise -->
<record id="source_demo_odoo_enterprise" model="cx.tower.git.source">
<field name="name">Odoo Enterprise (Private)</field>
<field name="reference">odoo_enterprise</field>
<field name="git_project_id" ref="git_project_demo" />
</record>
<!-- Remotes -->
<record id="remote_demo_odoo_enterprise" model="cx.tower.git.remote">
<field name="source_id" ref="source_demo_odoo_enterprise" />
<field name="repo_id" ref="repo_demo_odoo_enterprise" />
<field name="head_type">branch</field>
<field name="head">19.0</field>
</record>
<!-- Sample Private Gitlab -->
<record id="source_demo_gitlab_private" model="cx.tower.git.source">
<field name="name">Sample Semi Private Gitlab</field>
<field name="reference">gitlab_private</field>
<field name="git_project_id" ref="git_project_demo" />
</record>
<!-- Remotes -->
<record id="remote_demo_gitlab_private_main" model="cx.tower.git.remote">
<field name="source_id" ref="source_demo_gitlab_private" />
<field name="repo_id" ref="repo_demo_gitlab_private" />
<field name="head_type">branch</field>
<field name="head">main</field>
</record>
<record id="remote_demo_gitlab_private_mr_1234" model="cx.tower.git.remote">
<field name="source_id" ref="source_demo_gitlab_private" />
<field name="repo_id" ref="repo_demo_gitlab_private" />
<field name="head_type">pr</field>
<field name="head">1234</field>
</record>
<!-- Sample Private Bitbucket -->
<record id="source_demo_bitbucket_private" model="cx.tower.git.source">
<field name="name">Sample Private Bitbucket</field>
<field name="reference">bitbucket_private</field>
<field name="git_project_id" ref="git_project_demo" />
</record>
<!-- Remotes -->
<record id="remote_demo_bitbucket_private_main" model="cx.tower.git.remote">
<field name="source_id" ref="source_demo_bitbucket_private" />
<field name="repo_id" ref="repo_demo_bitbucket_private" />
<field name="head_type">branch</field>
<field name="head">dev</field>
</record>
<record id="remote_demo_bitbucket_private_feature" model="cx.tower.git.remote">
<field name="source_id" ref="source_demo_bitbucket_private" />
<field name="repo_id" ref="repo_demo_bitbucket_private" />
<field name="head_type">commit</field>
<field name="head">1234567890</field>
</record>
<!-- Files -->
<record id="file_demo_cetmix_tower_14_0_dev" model="cx.tower.file">
<field name="name">repos.yaml</field>
<field name="server_id" ref="cetmix_tower_server.server_demo_1" />
<field name="source">tower</field>
<field name="file_type">text</field>
<field name="server_dir">{{ instance_name }}/config</field>
</record>
<!-- Link file to git project -->
<record
id="git_project_rel_demo_cetmix_tower_14_0_dev"
model="cx.tower.git.project.rel"
>
<field name="git_project_id" ref="git_project_demo" />
<field name="server_id" ref="cetmix_tower_server.server_demo_1" />
<field name="file_id" ref="file_demo_cetmix_tower_14_0_dev" />
<field name="project_format">git_aggregator</field>
</record>
<!-- Demo variable for testing giturlparse -->
<record id="variable_demo_git_url" model="cx.tower.variable">
<field name="name">Demo Git URL</field>
<field name="reference">demo_git_url</field>
</record>
<!-- Demo command to test giturlparse -->
<record id="command_demo_git_url" model="cx.tower.command">
<field name="name">Parse Git URL</field>
<field name="action">python_code</field>
<field name="code">
if {{ demo_git_url }}:
parsed_url = giturlparse.parse({{ demo_git_url }})
repo = parsed_url.repo
owner = parsed_url.owner
host = parsed_url.host
platform = parsed_url.platform
message = "Repo: " + repo + ", Owner: " + owner + ", Host: " + host + ", Platform: " + platform
result={"exit_code": 0, "message": message}
else:
result={"exit_code": -100, "message": "Git URL is not defined!"}
</field>
<field name="access_level">1</field>
<field
name="tag_ids"
eval="[(6, 0, [ref('cetmix_tower_server.tag_custom')])]"
/>
<field name="note">Run Python Code: Check Branch</field>
</record>
</odoo>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,595 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * cetmix_tower_git
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server \n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fi\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0
#, python-format
msgid ""
"\n"
"# You need to set the following variables in your environment:\n"
"# %(vars)s \n"
"# and run git-aggregator with '--expand-env' parameter.\n"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0
#, python-format
msgid ""
"# This file is generated with Cetmix Tower https://cetmix.com/tower\n"
"# It's designed to be used with git-aggregator tool developed by Acsone.\n"
"# Documentation for git-aggregator: https://github.com/acsone/git-aggregator\n"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "* Sources where all remotes are private"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "* Sources where some remotes are private"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid ""
"<b>Managers.</b> All users who have \"Manager\" group and are set as \"Managers\" in <b><u>all</u></b> related servers.\n"
" This is done to avoid unpredictable consequences when some of the servers are not updated due to access restrictions when a project is updated."
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid ""
"<b>Users.</b> All users who have \"Manager\" group and are either set in "
"\"Users\" or in \"Managers\" in <b><u>all</u></b> related servers."
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Access"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__active
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__active
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__active
msgid "Active"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Archived"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__bitbucket
msgid "Bitbucket"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__branch
msgid "Branch"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form
msgid "Branch/PR/commit number or link"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__reference
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__reference
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__reference
msgid ""
"Can contain English letters, digits and '_'. Leave blank to autogenerate"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project
msgid "Cetmix Tower Git Configuration"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_remote
msgid "Cetmix Tower Git Remote"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_source
msgid "Cetmix Tower Git Source"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_server
msgid "Cetmix Tower Server"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0
#, python-format
msgid "Code generator function for '%(project_format)s' format not found."
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__commit
msgid "Commit"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_uid
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_uid
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_uid
msgid "Created by"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_date
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_date
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_date
msgid "Created on"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_file
msgid "Cx Tower File"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Disabled"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__display_name
msgid "Display Name"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__enabled
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__enabled
msgid "Enable in configuration and exported to files"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__enabled
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__enabled
msgid "Enabled"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Export YAML"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__file_id
msgid "File"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0
#, python-format
msgid "File '%(file)s' doesn't belong to server '%(server)s'"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_file.py:0
#, python-format
msgid ""
"File '%(file)s' is related to multiple projects: %(projects)s \n"
"Please select only one project."
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.constraint,message:cetmix_tower_git.constraint_cx_tower_git_project_rel_project_server_file_format_uniq
msgid "File is already related to the same project and format"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__file_ids
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Files"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__project_format
msgid "Format"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir
msgid "Git Aggregator Root Dir"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid ""
"Git Aggregator: Bitbucket does not support fetching PRs. Please use branch instead.\n"
"\n"
"Source: %(src)s\n"
"URL: %(url)s\n"
"Head: %(head)s"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid "Git Aggregator: Head number is empty in %(head)s"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__git_project_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__git_project_id
msgid "Git Configuration"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__git_project_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_ids
msgid "Git Project"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_rel_ids
msgid "Git Project Rel"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_project_rel_ids
msgid "Git Project Server File Relations"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project_rel
msgid "Git Project relation to other model records"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.actions.act_window,name:cetmix_tower_git.cx_tower_git_project_action
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_ids
#: model:ir.ui.menu,name:cetmix_tower_git.menu_cx_tower_git_project
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form
msgid "Git Projects"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir
msgid ""
"Git aggregator root directory where sources will be cloned. Eg '/tmp/git-"
"aggregator' Will use '.' if not set"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid ""
"Git aggregator root directory where sources will be cloned. Leave blank to "
"use '.'"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__url
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form
msgid ""
"Git remote URL. Eg 'https://github.com/cetmix/cetmix-tower.git' or "
"'git@github.com:cetmix/cetmix-tower.git'"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__head
msgid ""
"Git remote head. Link to branch, PR, commit or commit hash. Leave blank to "
"auto-detect"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__github
msgid "GitHub"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__gitlab
msgid "GitLab"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__https
msgid "HTTPS"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes
msgid "Has Partially Private Remotes"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes
msgid "Has Private Remotes"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head
msgid "Head"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head_type
msgid "Head Type"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__id
msgid "ID"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes
msgid "Indicates if the project has any partially private remotes."
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes
msgid "Indicates if the project has any private remotes."
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__is_private
msgid "Is Private"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file____last_update
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project____last_update
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel____last_update
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote____last_update
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source____last_update
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server____last_update
msgid "Last Modified on"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_uid
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_uid
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_uid
msgid "Last Updated by"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_date
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_date
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_date
msgid "Last Updated on"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__manager_ids
msgid "Managers"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__manager_ids
msgid "Managers who can modify this record"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__name
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Name"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid "Not a valid URL. URL must end with '.git'"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid "Not a valid URL. URL must start with 'https://' or 'git@'"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid ""
"Not a valid URL: %(url_msg)s\n"
"URL must contain at least two parts separated by dot."
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__other
msgid "Other"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Private"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count_private
msgid "Private Remotes"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__pr
msgid "Pull/Merge Request"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__reference
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__reference
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__reference
msgid "Reference"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid ""
"Reference. Can contain English letters, digits and '_'. Leave blank to "
"autogenerate"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_ids
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Remotes"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__repo_provider
msgid "Repository Provider"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__is_private
msgid "Repository is private"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__ssh
msgid "SSH"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__sequence
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__sequence
msgid "Sequence"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__server_id
msgid "Server"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__server_ids
msgid "Servers"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__server_ids
msgid ""
"Servers are added automatically based on the files linked to the project.\n"
"IMPORTANT: This field may contain duplicates because of the relation nature!"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__source_id
msgid "Source"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__source_ids
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Sources"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid ""
"The top one remote will be used as a merge target.\n"
" You can re-arrange remotes by dragging them or changing their sequence value."
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url
msgid "URL"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url_protocol
msgid "URL Protocol"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid "URL is required"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__user_ids
msgid "Users"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__user_ids
msgid "Users who can view this record"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__repo_provider
msgid ""
"Will be tried to be determined from the URL. Please select manually if auto-"
"detection fails."
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "YAML"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__yaml_code
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__yaml_code
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__yaml_code
msgid "Yaml Code"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid ""
"You can edit these fields at your own risk. However keep in mind that they "
"will be automatically updated each time related servers are added, removed "
"or updated."
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "You must be a member of the \"YAML/Export\" group to export data as YAML."
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "managers who can modify this record"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "users who can view this record"
msgstr ""

View File

@@ -0,0 +1,635 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * cetmix_tower_git
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 09:11+0000\n"
"Last-Translator: Bole <bole@dajmi5.com>\n"
"Language-Team: Croatian <https://hosted.weblate.org/projects/"
"tower-server-14-0-dev/cetmix_tower_git/hr/>\n"
"Language: hr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 5.10.3-dev\n"
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0
#, python-format
msgid ""
"\n"
"# You need to set the following variables in your environment:\n"
"# %(vars)s \n"
"# and run git-aggregator with '--expand-env' parameter.\n"
msgstr ""
"\n"
"# Potrebno je postaviti sljedeće varijable u vaše okruženje:\n"
"# %(vars)s \n"
"# i pokrenuti git-aggregator sa ' --expand-env' parametrom\n"
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0
#, python-format
msgid ""
"# This file is generated with Cetmix Tower https://cetmix.com/tower\n"
"# It's designed to be used with git-aggregator tool developed by Acsone.\n"
"# Documentation for git-aggregator: https://github.com/acsone/git-aggregator\n"
msgstr ""
"# Ova datotek aje generiran pomoću Cetmix Tower sustava: https://cetmix.com/"
"tower\n"
"# Dizajniran je za korištenje sa git-aggregator alatom razvijenim od Ascone."
"\n"
"# Dokumentacija za git-aggregator : https://github.com/acsone/git-"
"aggregator\n"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "* Sources where all remotes are private"
msgstr "* izvori sa svim udaljenim lokacijama koje su privatne"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "* Sources where some remotes are private"
msgstr "* Izvori u kojima su neke udaljene lokacije privatne"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid ""
"<b>Managers.</b> All users who have \"Manager\" group and are set as \"Managers\" in <b><u>all</u></b> related servers.\n"
" This is done to avoid unpredictable consequences when some of the servers are not updated due to access restrictions when a project is updated."
msgstr ""
"<b>Manageri.</b> Svi korisnici koji imaju \"Manager\" grupu i postavljeni su "
"kao \"Manageri\" u <b><u>svim</u></b> povezanim serverima.\n"
" "
"Ovo je napravljeno kako bi izbjegli nepredviđene posljedice kad neki od "
"servera nisu ažurirani zbog ograničenog pristupa prilikom ažuriranja "
"projekta."
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid ""
"<b>Users.</b> All users who have \"Manager\" group and are either set in "
"\"Users\" or in \"Managers\" in <b><u>all</u></b> related servers."
msgstr ""
"<b>Korisnici.</b> Svi korisnici koji imaju \"Manager\" grupu i postavljeni "
"su ili kao \"Korisnik\" ili kao \"Manager\" u <b><u>svim</u></b> povezanim "
"serverima."
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Access"
msgstr "Pristup"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__active
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__active
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__active
msgid "Active"
msgstr "Aktivno"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Archived"
msgstr "Arhivirano"
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__bitbucket
msgid "Bitbucket"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__branch
msgid "Branch"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form
msgid "Branch/PR/commit number or link"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__reference
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__reference
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__reference
msgid ""
"Can contain English letters, digits and '_'. Leave blank to autogenerate"
msgstr ""
"Može sadržavati slova engleske abecede, brojke i ':'. Ostavite prazno za "
"automatsko generiranje"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project
msgid "Cetmix Tower Git Configuration"
msgstr "Cetmix Tower Git postavke"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_remote
msgid "Cetmix Tower Git Remote"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_source
msgid "Cetmix Tower Git Source"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_server
msgid "Cetmix Tower Server"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0
#, python-format
msgid "Code generator function for '%(project_format)s' format not found."
msgstr ""
"Funkcija generiranja koda za '%(project_format)s' format nije pronađena."
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__commit
msgid "Commit"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_uid
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_uid
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_uid
msgid "Created by"
msgstr "Kreirao"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_date
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_date
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_date
msgid "Created on"
msgstr "Kreirano"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_file
msgid "Cx Tower File"
msgstr "Cx Tower datoteka"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Disabled"
msgstr "Onemogućen"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__display_name
msgid "Display Name"
msgstr "Naziv"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__enabled
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__enabled
msgid "Enable in configuration and exported to files"
msgstr "Omogućen u postavkama i izvezen u datoteku"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__enabled
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__enabled
msgid "Enabled"
msgstr "Omogućen"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Export YAML"
msgstr "Izvoz YAML"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__file_id
msgid "File"
msgstr "Datoteka"
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0
#, python-format
msgid "File '%(file)s' doesn't belong to server '%(server)s'"
msgstr "Datoteka '%(file)s' ne pripada serveru '%(server)s'"
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_file.py:0
#, python-format
msgid ""
"File '%(file)s' is related to multiple projects: %(projects)s \n"
"Please select only one project."
msgstr ""
"Datoteka '%(file)s' je povezana sa višestrukim projektima: %(projects)s \n"
"Molim odaberite samo jedan projekt."
#. module: cetmix_tower_git
#: model:ir.model.constraint,message:cetmix_tower_git.constraint_cx_tower_git_project_rel_project_server_file_format_uniq
msgid "File is already related to the same project and format"
msgstr "Datoteka je već povezana sa ovim projektom i ovim formatom"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__file_ids
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Files"
msgstr "Datoteke"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__project_format
msgid "Format"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir
msgid "Git Aggregator Root Dir"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid ""
"Git Aggregator: Bitbucket does not support fetching PRs. Please use branch instead.\n"
"\n"
"Source: %(src)s\n"
"URL: %(url)s\n"
"Head: %(head)s"
msgstr ""
"Git Aggregator: BitBucket ne podržava dohvaćanje PRova. Molim koristite "
"branch.\n"
"\n"
"Source: %(src)s\n"
"URL: %(url)s\n"
"Head: %(head)s"
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid "Git Aggregator: Head number is empty in %(head)s"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__git_project_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__git_project_id
msgid "Git Configuration"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__git_project_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_ids
msgid "Git Project"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_rel_ids
msgid "Git Project Rel"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_project_rel_ids
msgid "Git Project Server File Relations"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project_rel
msgid "Git Project relation to other model records"
msgstr "Git projekt povezan sa ostalim zapisima modela"
#. module: cetmix_tower_git
#: model:ir.actions.act_window,name:cetmix_tower_git.cx_tower_git_project_action
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_ids
#: model:ir.ui.menu,name:cetmix_tower_git.menu_cx_tower_git_project
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form
msgid "Git Projects"
msgstr "Git projekti"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir
msgid ""
"Git aggregator root directory where sources will be cloned. Eg '/tmp/git-"
"aggregator' Will use '.' if not set"
msgstr ""
"GitAgregator izvorni direktorij u koji će izvori biti klonirani. Npr. '/tmp/"
"git-aggregator' Ako ništa nije postavljeno koristi se '.'"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid ""
"Git aggregator root directory where sources will be cloned. Leave blank to "
"use '.'"
msgstr ""
"GitAgregtor izvorni dirketorij u koji će izvori biti klonirani. Ostavite "
"prazno za korištenje '.'"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__url
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form
msgid ""
"Git remote URL. Eg 'https://github.com/cetmix/cetmix-tower.git' or "
"'git@github.com:cetmix/cetmix-tower.git'"
msgstr ""
"Git remote URL. Eg 'https://github.com/cetmix/cetmix-tower.git' ili "
"'git@github.com:cetmix/cetmix-tower.git'"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__head
msgid ""
"Git remote head. Link to branch, PR, commit or commit hash. Leave blank to "
"auto-detect"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__github
msgid "GitHub"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__gitlab
msgid "GitLab"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__https
msgid "HTTPS"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes
msgid "Has Partially Private Remotes"
msgstr "Ima djelomično privatne udaljene izvore"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes
msgid "Has Private Remotes"
msgstr "Ima privatne udaljene izvore"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head
msgid "Head"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head_type
msgid "Head Type"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__id
msgid "ID"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes
msgid "Indicates if the project has any partially private remotes."
msgstr "Indicira ima li projekt djelomično privatnih udaljenih izvora."
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes
msgid "Indicates if the project has any private remotes."
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__is_private
msgid "Is Private"
msgstr "Je privatno"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file____last_update
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project____last_update
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel____last_update
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote____last_update
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source____last_update
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server____last_update
msgid "Last Modified on"
msgstr "Zadnje modificirano"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_uid
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_uid
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_uid
msgid "Last Updated by"
msgstr "Zadnji ažurirao"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_date
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_date
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_date
msgid "Last Updated on"
msgstr "Zadnje ažurirano"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__manager_ids
msgid "Managers"
msgstr "Manageri"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__manager_ids
msgid "Managers who can modify this record"
msgstr "Manageri koji mogu urediti ovaj zapis"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__name
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Name"
msgstr "Naziv"
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid "Not a valid URL. URL must end with '.git'"
msgstr "Nije valjani URL. URL mora završavati sa '.git'"
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid "Not a valid URL. URL must start with 'https://' or 'git@'"
msgstr "Nije valjani URL. URL mora počinjati sa 'https://' ili 'git@'"
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid ""
"Not a valid URL: %(url_msg)s\n"
"URL must contain at least two parts separated by dot."
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__other
msgid "Other"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Private"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count_private
msgid "Private Remotes"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__pr
msgid "Pull/Merge Request"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__reference
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__reference
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__reference
msgid "Reference"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid ""
"Reference. Can contain English letters, digits and '_'. Leave blank to "
"autogenerate"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_ids
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Remotes"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__repo_provider
msgid "Repository Provider"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__is_private
msgid "Repository is private"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__ssh
msgid "SSH"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__sequence
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__sequence
msgid "Sequence"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__server_id
msgid "Server"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__server_ids
msgid "Servers"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__server_ids
msgid ""
"Servers are added automatically based on the files linked to the project.\n"
"IMPORTANT: This field may contain duplicates because of the relation nature!"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__source_id
msgid "Source"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__source_ids
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Sources"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid ""
"The top one remote will be used as a merge target.\n"
" You can re-arrange remotes by dragging them or changing their sequence value."
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url
msgid "URL"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url_protocol
msgid "URL Protocol"
msgstr ""
#. module: cetmix_tower_git
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid "URL is required"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__user_ids
msgid "Users"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__user_ids
msgid "Users who can view this record"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__repo_provider
msgid ""
"Will be tried to be determined from the URL. Please select manually if auto-"
"detection fails."
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "YAML"
msgstr ""
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__yaml_code
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__yaml_code
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__yaml_code
msgid "Yaml Code"
msgstr "YAML kod"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid ""
"You can edit these fields at your own risk. However keep in mind that they "
"will be automatically updated each time related servers are added, removed "
"or updated."
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "You must be a member of the \"YAML/Export\" group to export data as YAML."
msgstr "Morate bit član \"YAML/Izvoz\" grupe za izvoz podataka u YAML."
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "managers who can modify this record"
msgstr ""
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "users who can view this record"
msgstr ""

View File

@@ -0,0 +1,740 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * cetmix_tower_git
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 2025-11-17 10:33+0100\n"
"Last-Translator: Stefano Consolaro <stefano.consolaro@mymage.it>\n"
"Language-Team: Italian <https://hosted.weblate.org/projects/tower-server-14-0-dev/cetmix_tower_git/it/>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: it\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Poedit 2.3\n"
#. module: cetmix_tower_git
#. odoo-python
#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0
#, python-format
msgid ""
"\n"
"# You need to set the following variables in your environment:\n"
"# %(vars)s \n"
"# and run git-aggregator with '--expand-env' parameter.\n"
msgstr ""
"\n"
"# È necessario impostare le seguenti variabili d'ambiente:\n"
"# %(vars)s \n"
"# ed eseguire git-aggregator con il parametro '--expand-env'.\n"
#. module: cetmix_tower_git
#. odoo-python
#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0
#, python-format
msgid ""
"# This file is generated with Cetmix Tower https://cetmix.com/tower\n"
"# It's designed to be used with git-aggregator tool developed by Acsone.\n"
"# Documentation for git-aggregator: https://github.com/acsone/git-aggregator\n"
msgstr ""
"# Questo file è generato con Cetmix Tower https://cetmix.com/tower\n"
"# È progettato per essere usato con il tool git-aggregator sviluppato da Acsone.\n"
"# Documentazione per git-aggregator: https://github.com/acsone/git-aggregator\n"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "* Sources where all remotes are private"
msgstr "* Origini dove tutti i remote sono privati"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "* Sources where some remotes are private"
msgstr "* Origini dove alcuni remote sono privati"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid ""
"<b>Managers.</b> All users who have \"Manager\" group and are set as \"Managers\" in <b><u>all</u></b> related servers.\n"
" This is done to avoid unpredictable consequences when some of the servers are not updated due to access restrictions when a project is updated."
msgstr ""
"<b>Responsabili.</b> Tutti gli utenti che hanno il gruppo \"Responsabile\" e sono impostati come \"Responsabili\" in <b><u>tutti</u></b> i server relativi.\n"
" Questo è fatto per evitare conseguenze imprevedibili quando qualcuno dei server non è aggiornato per limitazioni di accesso quando viene aggiornato un progetto."
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "<b>Users.</b> All users who have \"Manager\" group and are either set in \"Users\" or in \"Managers\" in <b><u>all</u></b> related servers."
msgstr "<b>Utenti.</b> Tutti gli utenti che hanno il gruppo \"Responsabile\" e sono impostati come \"Utenti\" o \"Responsabili\" in <b><u>tutti</u></b> i server relativi."
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Access"
msgstr "Accesso"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__active
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__active
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__active
msgid "Active"
msgstr "Attivo"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Archived"
msgstr "In archivio"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__auto_sync
msgid "Auto Sync"
msgstr "Sincronizzazione automatica"
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__bitbucket
msgid "Bitbucket"
msgstr "Bitbucket"
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__branch
msgid "Branch"
msgstr "Branch"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form
msgid "Branch/PR/commit number or link"
msgstr "Numero o link branch/PR/commit"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__reference
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project_rel__reference
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__reference
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__reference
msgid "Can contain English letters, digits and '_'. Leave blank to autogenerate"
msgstr "Può contenere lettere inglesi, cifre e '_'. Lasciare vuoto per la generazione automatica"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_file
msgid "Cetmix Tower File"
msgstr "File Cetmix Tower"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_file_template
msgid "Cetmix Tower File Template"
msgstr "Modello file Cetmix Tower"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_plan_line
msgid "Cetmix Tower Flight Plan Line"
msgstr "Riga piano di volo Cetmix Tower"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project
msgid "Cetmix Tower Git Project"
msgstr "Progetto Git Cetmix Tower"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project_file_template_rel
msgid "Cetmix Tower Git Project relation to File Templates"
msgstr "Relazione progetto Git Cetmix Tower al modello file"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project_rel
msgid "Cetmix Tower Git Project relation to Files and Servers"
msgstr "Relazione progetto Git Cetmix Tower a file e server"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_remote
msgid "Cetmix Tower Git Remote"
msgstr "Remote Git Cetmix Tower"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_source
msgid "Cetmix Tower Git Source"
msgstr "Origine Git Cetmix Tower"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cetmix_tower
msgid "Cetmix Tower Odoo Automation"
msgstr "Automazione Odoo Cetmix Tower"
#. module: cetmix_tower_git
#: model:ir.model,name:cetmix_tower_git.model_cx_tower_server
msgid "Cetmix Tower Server"
msgstr "Server Cetmix Tower"
#. module: cetmix_tower_git
#. odoo-python
#: code:addons/cetmix_tower_git/models/cx_tower_git_project_file_template_rel.py:0
#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0
#, python-format
msgid "Code generator function for '%(project_format)s' format not found."
msgstr "Funzione generazione codice per il formato '%(project_format)s' non trovata."
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__commit
msgid "Commit"
msgstr "Commit"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_plan_line__is_make_copy
msgid "Create a copy of the Git Project instead of linking the file to the existing one."
msgstr "Crea una copia del progetto Git invece di collegare il file a uno esistente."
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_uid
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_uid
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_uid
msgid "Created by"
msgstr "Creato da"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_date
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_date
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_date
msgid "Created on"
msgstr "Creato il"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Disabled"
msgstr "Disabilitato"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__display_name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__display_name
msgid "Display Name"
msgstr "Nome visualizzato"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__enabled
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__enabled
msgid "Enable in configuration and exported to files"
msgstr "Abilitato in configurazione ed esportato nei file"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__enabled
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__enabled
msgid "Enabled"
msgstr "Abilitato"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Export YAML"
msgstr "Esporta YAML"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__file_id
msgid "File"
msgstr "File"
#. module: cetmix_tower_git
#. odoo-python
#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0
#, python-format
msgid "File '%(file)s' doesn't belong to server '%(server)s'"
msgstr "Il file '%(file)s' non appartiene al server '%(server)s'"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__file_template_id
msgid "File Template"
msgstr "Modello file"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__file_template_ids
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "File Templates"
msgstr "Modelli file"
#. module: cetmix_tower_git
#: model:ir.model.constraint,message:cetmix_tower_git.constraint_cx_tower_git_project_rel_project_server_file_format_uniq
msgid "File is already related to the same project and format"
msgstr "Il file è già associato allo stesso progetto e formato"
#. module: cetmix_tower_git
#: model:ir.model.constraint,message:cetmix_tower_git.constraint_cx_tower_git_project_file_template_rel_project_server_file_format_uniq
msgid "File template is already related to the same project and format"
msgstr "Il modello di file è già associato allo stesso progetto e formato"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__file_ids
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Files"
msgstr "File"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__project_format
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__project_format
msgid "Format"
msgstr "Formato"
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__git
msgid "GIT"
msgstr "GIT"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "General"
msgstr "Generale"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Git Aggregator"
msgstr "Git Aggregator"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir
msgid "Git Aggregator Root Dir"
msgstr "Directory radice Git Aggregator"
#. module: cetmix_tower_git
#. odoo-python
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid ""
"Git Aggregator: Bitbucket does not support fetching PRs. Please use branch instead.\n"
"\n"
"Source: %(src)s\n"
"URL: %(url)s\n"
"Head: %(head)s"
msgstr ""
"Git Aggregator: Bitbucket non supporta il recupero delle PR. In alternativa usare il branch.\n"
"\n"
"Origine: %(src)s\n"
"URL: %(url)s\n"
"Head: %(head)s"
#. module: cetmix_tower_git
#. odoo-python
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid "Git Aggregator: Head number is empty in %(head)s"
msgstr "Git Aggregator: il numero dell'head è vuoto in %(head)s"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__git_project_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__git_project_id
msgid "Git Configuration"
msgstr "Configurazione Git"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file_template__git_project_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__git_project_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__git_project_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_plan_line__git_project_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_ids
msgid "Git Project"
msgstr "Progetto Git"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_project_file_template_rel_ids
msgid "Git Project File Template Relations"
msgstr "Relazioni modello file progetto Git"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_rel_ids
msgid "Git Project Rel"
msgstr "Rel progetto Git"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_rel_ids
msgid "Git Project Relations"
msgstr "Relazioni progetto Git"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_project_rel_ids
msgid "Git Project Server File Relations"
msgstr "Relazioni file server progetto Git"
#. module: cetmix_tower_git
#: model:ir.actions.act_window,name:cetmix_tower_git.cx_tower_git_project_action
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file_template__git_project_ids
#: model:ir.ui.menu,name:cetmix_tower_git.menu_cx_tower_git_project
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form
msgid "Git Projects"
msgstr "Progetti Git"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir
msgid "Git aggregator root directory where sources will be cloned. Eg '/tmp/git-aggregator' Will use '.' if not set"
msgstr "La directory radice di Git Aggregator dove le origini verranno clonate. Es. '/tmp/git-aggregator'. Verrà usato '.' se non impostata"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Git aggregator root directory where sources will be cloned. Leave blank to use '.'"
msgstr "La directory radice di Git Aggregator dove le origini verranno clonate. Lasciare vuoto per usare '.'"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__url
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form
msgid "Git remote URL. Eg 'https://github.com/cetmix/cetmix-tower.git' or 'git@github.com:cetmix/cetmix-tower.git'"
msgstr "URL del remote Git. Es. 'https://github.com/cetmix/cetmix-tower.git' o 'git@github.com:cetmix/cetmix-tower.git'"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__head
msgid "Git remote head. Link to branch, PR, commit or commit hash. Leave blank to auto-detect"
msgstr "Head remoto Git. Collegamento al branch, alla PR, al commit o all'hash del commit. Lasciare vuoto per il rilevamento automatico"
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__github
msgid "GitHub"
msgstr "GitHub"
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__gitlab
msgid "GitLab"
msgstr "GitLab"
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__https
msgid "HTTPS"
msgstr "HTTPS"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes
msgid "Has Partially Private Remotes"
msgstr "Ha remote parzialmente privati"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes
msgid "Has Private Remotes"
msgstr "Ha remote privati"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head
msgid "Head"
msgstr "Head"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head_type
msgid "Head Type"
msgstr "Tipo di head"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__id
msgid "ID"
msgstr "ID"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project_rel__auto_sync
msgid "If enabled file will be synced automatically using cron"
msgstr "Se abilitato, il file verrà sincronizzato automaticamente tramite cron"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes
msgid "Indicates if the project has any partially private remotes."
msgstr "Indica se il progetto ha qualche remote parzialmente privato."
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes
msgid "Indicates if the project has any private remotes."
msgstr "Indica se il progetto ha qualche remote privato."
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__is_private
msgid "Is Private"
msgstr "È privato"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_uid
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_uid
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_uid
msgid "Last Updated by"
msgstr "Ultimo aggiornamento di"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_date
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_date
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_date
msgid "Last Updated on"
msgstr "Ultimo aggiornamento il"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_plan_line__is_make_copy
msgid "Make a Copy"
msgstr "Crea una copia"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__manager_ids
msgid "Managers"
msgstr "Responsabili"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__manager_ids
msgid "Managers who can modify this record"
msgstr "Responsabili che possono modificare questo record"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__name
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__name
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Name"
msgstr "Nome"
#. module: cetmix_tower_git
#. odoo-python
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid "Not a valid URL. URL must end with '.git'"
msgstr "URL non valido. L'URL deve terminare con '.git'"
#. module: cetmix_tower_git
#. odoo-python
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid "Not a valid URL. URL must start with 'https://', 'git@', or 'git://'"
msgstr "URL non valido. L'URL deve iniziare con 'https://', 'git@' o 'git://'"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__note
msgid "Note"
msgstr "Nota"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form
msgid "Open"
msgstr "Apri"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Open File Template"
msgstr "Apri modello file"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form
msgid "Open Git Project"
msgstr "Apri progetto Git"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Open Server"
msgstr "Apri server"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Open file template"
msgstr "Apri modello file"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Open server"
msgstr "Apri server"
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__other
msgid "Other"
msgstr "Altro"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Private"
msgstr "Privato"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count_private
msgid "Private Remotes"
msgstr "Remote privati"
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__pr
msgid "Pull/Merge Request"
msgstr "Pull/Merge Request"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Put your notes here..."
msgstr "Inserire qui le note..."
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__reference
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__reference
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__reference
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__reference
msgid "Reference"
msgstr "Riferimento"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Reference. Can contain English letters, digits and '_'. Leave blank to autogenerate"
msgstr "Riferimento. Può contenere lettere inglesi, cifre e '_'. Lasciare vuoto per la generazione automatica"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_ids
msgid "Remote"
msgstr "Remote"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid "Remotes"
msgstr "Remote"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__repo_provider
msgid "Repository Provider"
msgstr "Fornitore repository"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__is_private
msgid "Repository is private"
msgstr "Il repository è privato"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Root Directory"
msgstr "Cartella radice"
#. module: cetmix_tower_git
#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__ssh
msgid "SSH"
msgstr "SSH"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_plan_line__git_project_id
#, fuzzy
msgid "Select a git project to be linked to the file and server."
msgstr "Seleziona un progetto Git da collegare al file e al server."
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__sequence
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__sequence
msgid "Sequence"
msgstr "Sequenza"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__server_id
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__server_id
msgid "Server"
msgstr "Server"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__server_ids
msgid "Servers"
msgstr "Server"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__server_ids
msgid ""
"Servers are added automatically based on the files linked to the project.\n"
"IMPORTANT: This field may contain duplicates because of the relation nature!"
msgstr ""
"I server sono aggiunti automaticamente in base ai file collegati al progetto.\n"
"IMPORTANTE: questo campo può contenere duplicati a causa della natura della relazione!"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__source_id
msgid "Source"
msgstr "Origine"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__source_ids
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "Sources"
msgstr "Origini"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form
msgid ""
"The top one remote will be used as a merge target.\n"
" You can re-arrange remotes by dragging them or changing their sequence value."
msgstr ""
"Il remote in cima verrà utilizzato come destinazione del merge.\n"
" È possibile riorganizzare i remote trascinandoli o modificando il loro valore di sequenza."
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url
msgid "URL"
msgstr "URL"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url_protocol
msgid "URL Protocol"
msgstr "Protocollo URL"
#. module: cetmix_tower_git
#. odoo-python
#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0
#, python-format
msgid "URL is required"
msgstr "È richiesto l'URL"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__user_ids
msgid "Users"
msgstr "Utenti"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__user_ids
msgid "Users who can view this record"
msgstr "Utenti che possono vedere questo record"
#. module: cetmix_tower_git
#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__repo_provider
msgid "Will be tried to be determined from the URL. Please select manually if auto-detection fails."
msgstr "Si cercherà di determinarlo dall'URL. Selezionarlo manualmente se fallisce l'auto determinazione."
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "YAML"
msgstr "YAML"
#. module: cetmix_tower_git
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__yaml_code
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__yaml_code
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__yaml_code
#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__yaml_code
msgid "Yaml Code"
msgstr "Codice YAML"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "You can edit these fields at your own risk. However keep in mind that they will be automatically updated each time related servers are added, removed or updated."
msgstr "È possibile modificare questi campi a proprio rischio. Tuttavia, tenere presente che verranno aggiornati automaticamente ogni volta che i server correlati vengono aggiunti, rimossi o aggiornati."
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "You must be a member of the \"YAML/Export\" group to export data as YAML."
msgstr "Bisogna appartenere al gruppo \"YAML/Export\" per esportare dati in YAML."
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "managers who can modify this record"
msgstr "responsabili che possono modificare questo record"
#. module: cetmix_tower_git
#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form
msgid "users who can view this record"
msgstr "utenti che possono visualizzare questo record"
#~ msgid ""
#~ "File '%(file)s' is related to multiple projects: %(projects)s \n"
#~ "Please select only one project."
#~ msgstr ""
#~ "Il file '%(file)s' è relativo a progetti multipli: %(projects)s \n"
#~ "Selezionare solo un progetto."
#~ msgid "Last Modified on"
#~ msgstr "Ultima modifica il"
#~ msgid ""
#~ "Not a valid URL: %(url_msg)s\n"
#~ "URL must contain at least two parts separated by dot."
#~ msgstr ""
#~ "URL non valdio: %(url_msg)s\n"
#~ "URL deve contenere almeno due parti separate da un punto."

View File

@@ -0,0 +1,15 @@
# cx_tower_git_project_rel must be the first one in the list
# in order to create the relation table properly
from . import cx_tower_git_project_rel
from . import cx_tower_git_project_file_template_rel
from . import cx_tower_file
from . import cx_tower_file_template
from . import cx_tower_git_project
from . import cx_tower_git_remote
from . import cx_tower_git_repo
from . import cx_tower_git_repo_owner
from . import cx_tower_git_source
from . import cx_tower_server
from . import cetmix_tower
from . import cx_tower_plan_line
from . import cx_tower_command

View File

@@ -0,0 +1,35 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models
class CetmixTower(models.AbstractModel):
_inherit = "cetmix.tower"
@api.model
def servers_by_git_ref(self, repository_url, head=None, head_type=None):
"""
Return servers linked to a given Git repository reference.
This is a thin shortcut that delegates to
:meth:`cx.tower.server.get_servers_by_git_ref`.
Parameters
----------
repository_url : str
Pre-normalized canonical Git URL
(e.g. ``https://host/owner/repo.git``).
head : str, optional
Branch name, commit SHA, or PR identifier.
head_type : {'branch', 'commit', 'pr'}, optional
Type of the ``head`` argument.
Returns
-------
recordset of cx.tower.server
Matching servers. Empty recordset if no matches.
"""
return self.env["cx.tower.server"].get_servers_by_git_ref(
repository_url, head, head_type
)

View File

@@ -0,0 +1,37 @@
# Copyright 2024 Cetmix OÜ
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, models
from odoo.tools.safe_eval import wrap_module
# Wrap giturlparse safely
giturlparse = wrap_module(__import__("giturlparse"), ["parse", "validate"])
class CxTowerCommand(models.Model):
"""Extends cx.tower.command to add giturlparse functionality."""
_inherit = "cx.tower.command"
def _custom_python_libraries(self):
"""
Add the giturlparse library to the available libraries.
"""
custom_python_libraries = super()._custom_python_libraries()
custom_python_libraries.update(
{
"cetmix_tower_git": {
"giturlparse": {
"import": giturlparse,
"help": _(
"Python library for Git URL parsing. "
"Available methods: 'parse', 'validate'. "
" <a "
"href='https://github.com/nephila/giturlparse/'"
" target='_blank'>Documentation on GitHub</a>."
),
},
}
}
)
return custom_python_libraries

View File

@@ -0,0 +1,54 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class CxTowerFile(models.Model):
_inherit = "cx.tower.file"
git_project_id = fields.Many2one(
comodel_name="cx.tower.git.project",
compute="_compute_git_project_id",
store=True,
)
git_project_rel_ids = fields.One2many(
comodel_name="cx.tower.git.project.rel",
inverse_name="file_id",
string="Git Project Relations",
copy=False,
)
# Get server from the first related git project relation
# This is needed for YAML import
server_id = fields.Many2one(
comodel_name="cx.tower.server",
compute="_compute_git_project_id",
store=True,
readonly=False,
)
@api.depends("git_project_rel_ids.server_id", "git_project_rel_ids.git_project_id")
def _compute_git_project_id(self):
"""
Link to project using the proxy model.
"""
for record in self:
# File is related to project via proxy model.
# So there can be only one record in o2m field.
git_project_relation = (
record.git_project_rel_ids and record.git_project_rel_ids[0]
)
if git_project_relation:
record.update(
{
"git_project_id": git_project_relation.git_project_id,
"server_id": git_project_relation.server_id,
}
)
else:
# Reset only git project id as file still belongs to the server
record.update(
{
"git_project_id": False,
}
)

View File

@@ -0,0 +1,32 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class CxTowerFileTemplate(models.Model):
_inherit = "cx.tower.file.template"
git_project_ids = fields.Many2many(
comodel_name="cx.tower.git.project",
relation="cx_tower_git_project_file_template_rel",
column1="file_template_id",
column2="git_project_id",
string="Git Projects",
copy=False,
)
git_project_id = fields.Many2one(
comodel_name="cx.tower.git.project",
compute="_compute_git_project_id",
)
@api.depends("git_project_ids")
def _compute_git_project_id(self):
"""
Link to project using the proxy model.
"""
for record in self:
# File is related to project via proxy model.
# So there can be only one record in o2m field.
record.git_project_id = (
record.git_project_ids and record.git_project_ids[0].id
)

View File

@@ -0,0 +1,368 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import re
from odoo import _, api, fields, models
class CxTowerGitProject(models.Model):
"""
Git Project.
Implements pre-defined git configuration.
"""
_name = "cx.tower.git.project"
_description = "Cetmix Tower Git Project"
_order = "name"
_inherit = [
"cx.tower.reference.mixin",
"cx.tower.yaml.mixin",
"cx.tower.access.role.mixin",
]
def _get_post_create_fields(self):
res = super()._get_post_create_fields()
return res + [
"source_ids",
"git_project_rel_ids",
"git_project_file_template_rel_ids",
]
active = fields.Boolean(default=True)
server_ids = fields.Many2many(
comodel_name="cx.tower.server",
relation="cx_tower_git_project_server_rel",
readonly=True,
copy=False,
compute="_compute_server_ids",
store=True,
context={"active_test": False},
help="Servers are added automatically based on the files"
" linked to the project.",
)
source_ids = fields.One2many(
comodel_name="cx.tower.git.source",
inverse_name="git_project_id",
string="Sources",
auto_join=True,
copy=True,
)
git_project_rel_ids = fields.One2many(
comodel_name="cx.tower.git.project.rel",
inverse_name="git_project_id",
string="Git Project Server File Relations",
copy=False,
)
# Helper field to get all files related to git project
file_ids = fields.Many2many(
comodel_name="cx.tower.file",
relation="cx_tower_git_project_rel",
column1="git_project_id",
column2="file_id",
string="Files",
readonly=True,
depends=["git_project_rel_ids"],
copy=False,
)
git_project_file_template_rel_ids = fields.One2many(
comodel_name="cx.tower.git.project.file.template.rel",
inverse_name="git_project_id",
string="Git Project File Template Relations",
copy=False,
)
# Helper field to get all file templates related to git project
file_template_ids = fields.Many2many(
comodel_name="cx.tower.file.template",
relation="cx_tower_git_project_file_template_rel",
column1="git_project_id",
column2="file_template_id",
string="File Templates",
readonly=True,
depends=["git_project_file_template_rel_ids"],
copy=False,
)
# Helper field to get all repositories used in this project
repo_ids = fields.Many2many(
comodel_name="cx.tower.git.repo",
relation="cx_tower_git_repo_project_rel",
column1="project_id",
column2="repo_id",
string="Repositories",
readonly=True,
copy=False,
help="Repositories used in this project through its sources and remotes",
)
note = fields.Text()
# ---- Access. Add relation for mixin fields
user_ids = fields.Many2many(
relation="cx_tower_git_project_user_rel",
compute="_compute_user_ids",
readonly=False,
store=True,
precompute=True,
)
manager_ids = fields.Many2many(
relation="cx_tower_git_project_manager_rel",
compute="_compute_user_ids",
readonly=False,
store=True,
precompute=True,
)
# -- UI/UX fields
has_private_remotes = fields.Boolean(
compute="_compute_has_private_remotes",
help="Indicates if the project has any private remotes.",
)
has_partially_private_remotes = fields.Boolean(
compute="_compute_has_private_remotes",
help="Indicates if the project has any partially private remotes.",
)
# -- Git Aggregator related fields
git_aggregator_root_dir = fields.Char(
help="Git aggregator root directory where sources will be cloned."
" Eg '/tmp/git-aggregator'"
" Will use '.' if not set",
)
def _selection_project_format(self):
"""
Possible project formats.
Inherit and extend when adding new project formats.
Returns:
List of tuples: (code, name)
"""
return [
("git_aggregator", "Git Aggregator"),
]
def _default_project_format(self):
"""
Default project format.
"""
return "git_aggregator"
@api.depends("git_project_rel_ids", "git_project_rel_ids.server_id")
def _compute_server_ids(self):
"""Compute server ids for git projects.
Why? Because a git project can be linked to multiple files
on the same server.
So we need to use a set to avoid duplicates so every server
is listed only once.
"""
for project in self:
project.server_ids = (
list(set(project.git_project_rel_ids.server_id.ids))
if project.git_project_rel_ids
else False
)
@api.depends(
"git_project_rel_ids.server_id",
"git_project_rel_ids.server_id.user_ids",
"git_project_rel_ids.server_id.manager_ids",
)
def _compute_user_ids(self):
"""
Users. All users who have "Manager" group and are either set in "Users"
or in "Managers" in all related servers.
Managers. All users who have "Manager" group and are set as "Managers"
in all related servers.
This is done to avoid unpredictable consequences when some of the servers
are not updated due to access restrictions when a project is updated.
"""
for project in self:
# Do not compute if no servers are related
server_ids = project.git_project_rel_ids.server_id
if not server_ids:
continue
# Get all user and manager ids from related servers
all_user_ids = server_ids.user_ids.filtered(
lambda u: u.has_group("cetmix_tower_server.group_manager")
).ids
all_manager_ids = server_ids.manager_ids.ids
# Create a final list of user and manager ids
user_ids = []
manager_ids = []
# Check if user is present in all servers
for user_id in all_user_ids:
if all(
user_id in server.user_ids.ids or user_id in server.manager_ids.ids
for server in server_ids
):
user_ids.append(user_id)
# Check if manager is present in all servers
for manager_id in all_manager_ids:
if all(manager_id in server.manager_ids.ids for server in server_ids):
manager_ids.append(manager_id)
# Set the final lists
project.update(
{
"user_ids": [(6, 0, user_ids)],
"manager_ids": [(6, 0, manager_ids)],
}
)
@api.depends(
"source_ids", "source_ids.remote_ids", "source_ids.remote_ids.is_private"
)
def _compute_has_private_remotes(self):
for project in self:
project.has_private_remotes = any(
source.remote_count > 0
and source.remote_count_private == source.remote_count
for source in project.source_ids
)
project.has_partially_private_remotes = any(
source.remote_count_private > 0
and source.remote_count_private != source.remote_count
for source in project.source_ids
)
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
# Update related files and templates on create
res._update_related_files_and_templates()
return res
def write(self, vals):
res = super().write(vals)
# Update related files and templates on update
self._update_related_files_and_templates()
return res
# ------------------------------
# Helper methods
# ------------------------------
def _update_related_files_and_templates(self):
# Update related files and templates
if self.git_project_rel_ids:
self.git_project_rel_ids._save_to_file()
if self.git_project_file_template_rel_ids:
self.git_project_file_template_rel_ids._save_to_file_template()
def _extract_variables_from_text(self, text):
"""Extract environment variables from text.
Helper method for file content generation.
Args:
text (str): Text to extract variables from
Returns:
List: List of variables
"""
# This regex will find all variables where variables are denoted
# as $VAR or ${VAR}, e.g., $FOO or ${FOO_BAR123}
variables = re.findall(r"\$\{?([A-Z0-9_]+)\}?", text)
return sorted(list(set(variables)))
def _compose_copy_name(self, server=False):
"""
Compose copy name of a git project copy.
Helper method used when creating a copy of a git project.
Args:
server (cx.tower.server): Server to get the copy name for.
Returns:
Char: Copy name
"""
self.ensure_one()
if server:
return server.name
return _("%(name)s (copy)", name=self.name)
# ------------------------------
# YAML mixin methods
# ------------------------------
def _get_fields_for_yaml(self):
res = super()._get_fields_for_yaml()
res += [
"name",
"note",
"source_ids",
"git_aggregator_root_dir",
]
return res
# -------------------------------
# Git Aggregator related methods
# -------------------------------
def _git_aggregator_prepare_record(self):
"""Prepare json structure for git aggregator.
Returns:
Dict: Json structure for git aggregator
"""
self.ensure_one()
values = {}
for source in self.source_ids:
if source.enabled and source.remote_count:
root_dir = self.git_aggregator_root_dir or "."
values.update(
{
f"/{source.reference}"
if root_dir == "/"
else f"{root_dir}/{source.reference}": source._git_aggregator_prepare_record() # noqa: E501
}
)
return values
def _git_aggregator_prepare_yaml_comment(self, yaml_code):
"""Generate commentary for yaml file.
It includes brief instructions for git aggregator
and lists environment variables that are required.
Args:
yaml_code (str): Yaml code
Returns:
Char: comment text or None
"""
comment_text = _(
"# This file is generated with Cetmix Tower https://cetmix.com/tower\n"
"# It's designed to be used with git-aggregator tool developed by Acsone.\n"
"# Documentation for git-aggregator: https://github.com/acsone/git-aggregator\n"
)
variable_list = self._extract_variables_from_text(yaml_code)
if variable_list:
comment_text += _(
"\n# You need to set the following variables in your environment:\n# %(vars)s\n" # noqa: E501
"# and run git-aggregator with '--expand-env' parameter.\n", # noqa: E501
vars=(", ".join(variable_list)),
)
return comment_text
def _generate_code_git_aggregator(self, record):
"""Generate code in git-aggregator format.
Args:
record (recordset()): Model record to generate code for.
must be a single record and have git_project_id field.
Returns:
Text: Yaml code
"""
yaml_mixin = self.env["cx.tower.yaml.mixin"]
# Do not generate code if record values are empty
record_values = record.git_project_id._git_aggregator_prepare_record()
if record_values:
yaml_code = yaml_mixin._convert_dict_to_yaml(record_values)
# Prepend comment to yaml code
comment = record.git_project_id._git_aggregator_prepare_yaml_comment(
yaml_code
)
return f"{comment}\n{yaml_code}"
return ""

View File

@@ -0,0 +1,115 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class CxTowerGitProjectFileTemplateRel(models.Model):
"""
Relation between git projects and file templates.
"""
_name = "cx.tower.git.project.file.template.rel"
_table = "cx_tower_git_project_file_template_rel"
_description = "Cetmix Tower Git Project relation to File Templates"
_log_access = False
name = fields.Char(related="git_project_id.name", readonly=True)
git_project_id = fields.Many2one(
comodel_name="cx.tower.git.project",
index=True,
required=True,
ondelete="cascade",
)
file_template_id = fields.Many2one(
comodel_name="cx.tower.file.template",
required=True,
ondelete="cascade",
)
project_format = fields.Selection(
selection=lambda self: self.env[
"cx.tower.git.project"
]._selection_project_format(),
default=lambda self: self.env["cx.tower.git.project"]._default_project_format(),
required=True,
string="Format",
)
_sql_constraints = [
(
"project_server_file_template_format_uniq",
"unique(git_project_id, file_template_id, project_format)",
"File template is already related to the same project and format",
),
]
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
# Export project to file
res._save_to_file_template()
return res
def write(self, vals):
res = super().write(vals)
# Export project to file
self._save_to_file_template()
return res
def action_open_file_template(self):
"""
Open file template record in current window
"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": self.file_template_id.name,
"res_model": "cx.tower.file.template",
"res_id": self.file_template_id.id, # pylint: disable=no-member
"view_mode": "form",
"view_type": "form",
"target": "current",
}
# ----------------------------------------------------
# Save project to linked file based on selected format
# ----------------------------------------------------
def _save_to_file_template(self):
"""Save project to linked file using format-specific function."""
# Get required function based on project format
# Following the pattern: _generate_code__<format> where format
# is one of the values in _selection_project_format
# Function gets a single record as an argument.
# Save resolved functions to dict for faster access
code_generator_functions = {}
for record in self:
code_generator_function = code_generator_functions.get(
record.project_format
)
if not code_generator_function:
code_generator_function = getattr(
record.git_project_id,
f"_generate_code_{record.project_format}",
None,
)
if not code_generator_function:
raise ValidationError(
_(
"Code generator function for '%(project_format)s'"
" format not found.",
project_format=record.project_format,
)
)
code_generator_functions[record.project_format] = (
code_generator_function
)
# Generate code for current record
code = code_generator_function(record)
if record.file_template_id.code != code:
record.file_template_id.write({"code": code})

View File

@@ -0,0 +1,179 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class CxTowerGitProjectRel(models.Model):
"""
Relation between git projects and other model records.
"""
_name = "cx.tower.git.project.rel"
_inherit = [
"cx.tower.reference.mixin",
"cx.tower.yaml.mixin",
]
_table = "cx_tower_git_project_rel"
_description = "Cetmix Tower Git Project relation to Files and Servers"
_log_access = False
name = fields.Char(related="git_project_id.name", readonly=True)
git_project_id = fields.Many2one(
comodel_name="cx.tower.git.project",
index=True,
required=True,
ondelete="cascade",
)
server_id = fields.Many2one(
comodel_name="cx.tower.server",
index=True,
required=True,
ondelete="cascade",
)
file_id = fields.Many2one(
comodel_name="cx.tower.file",
domain="[('server_id', '=', server_id),"
"('source', '=', 'tower'),"
"('file_type', '=', 'text')]",
required=True,
ondelete="cascade",
)
project_format = fields.Selection(
selection=lambda self: self.env[
"cx.tower.git.project"
]._selection_project_format(),
default=lambda self: self.env["cx.tower.git.project"]._default_project_format(),
required=True,
string="Format",
)
auto_sync = fields.Boolean(related="file_id.auto_sync", readonly=False)
_sql_constraints = [
(
"project_server_file_format_uniq",
"unique(git_project_id, file_id, project_format)",
"File is already related to the same project and format",
),
]
@api.constrains("server_id", "file_id")
def _check_server_file_relation(self):
"""
Check if server and file are related.
"""
for record in self:
if (
record.file_id.server_id
and record.server_id != record.file_id.server_id
):
raise ValidationError(
_(
"File '%(file)s' doesn't belong to server '%(server)s'",
file=record.file_id.name,
server=record.server_id.name,
)
)
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
# Export project to file
res._save_to_file()
return res
def write(self, vals):
res = super().write(vals)
# Export project to file
self._save_to_file()
return res
def action_open_project(self):
"""
Open project record in current window
"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": self.name,
"res_model": "cx.tower.git.project",
"res_id": self.git_project_id.id, # pylint: disable=no-member
"view_mode": "form",
"view_type": "form",
"target": "current",
}
def action_open_server(self):
"""
Open server record in current window
"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": self.server_id.name,
"res_model": "cx.tower.server",
"res_id": self.server_id.id, # pylint: disable=no-member
"view_mode": "form",
"view_type": "form",
"target": "current",
}
# ----------------------------------------------------
# Save project to linked file based on selected format
# ----------------------------------------------------
def _save_to_file(self):
"""Save project to linked file using format-specific function."""
# Get required function based on project format
# Following the pattern: _generate_code_<format> where format
# is one of the values in _selection_project_format
# Function gets a single record as an argument.
# Save resolved functions to dict for faster access
code_generator_functions = {}
for record in self:
# Disconnect file from file template if it is connected
if record.file_id.template_id:
record.file_id.action_unlink_from_template()
code_generator_function = code_generator_functions.get(
record.project_format
)
if not code_generator_function:
code_generator_function = getattr(
record.git_project_id,
f"_generate_code_{record.project_format}",
None,
)
if not code_generator_function:
raise ValidationError(
_(
"Code generator function for '%(project_format)s'"
" format not found.",
project_format=record.project_format,
)
)
code_generator_functions[record.project_format] = (
code_generator_function
)
# Generate code for current record
code = code_generator_function(record)
if record.file_id.code != code:
record.file_id.write({"code": code})
# ------------------------------
# YAML mixin methods
# ------------------------------
def _get_fields_for_yaml(self):
res = super()._get_fields_for_yaml()
res += [
"file_id",
"git_project_id",
"project_format",
"auto_sync",
]
return res

View File

@@ -0,0 +1,403 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import giturlparse
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class CxTowerGitRemote(models.Model):
"""
Git Remote.
Implements single git remote.
Eg a branch or a pull request.
"""
_name = "cx.tower.git.remote"
_inherit = [
"cx.tower.reference.mixin",
"cx.tower.yaml.mixin",
]
_description = "Cetmix Tower Git Remote"
_order = "sequence, name"
active = fields.Boolean(related="source_id.active", store=True, readonly=True)
enabled = fields.Boolean(
default=True, help="Enable in configuration and exported to files"
)
sequence = fields.Integer(default=10)
name = fields.Char(compute="_compute_name", store=True, default="remote")
source_id = fields.Many2one(
comodel_name="cx.tower.git.source",
required=True,
ondelete="cascade",
auto_join=True,
)
git_project_id = fields.Many2one(
comodel_name="cx.tower.git.project",
related="source_id.git_project_id",
store=True,
readonly=True,
)
repo_id = fields.Many2one(
comodel_name="cx.tower.git.repo",
string="Repository",
required=True,
ondelete="restrict",
help="If selected, the remote URL will be filled from the"
" repo settings based on the remote protocol",
)
repo_provider = fields.Selection(
related="repo_id.provider",
readonly=True,
)
# -- Repo related fields
url_protocol = fields.Selection(
string="Protocol",
selection=[
("ssh", "SSH"),
("https", "HTTPS"),
("git", "GIT"),
],
required=True,
default=lambda self: self._get_default_url_protocol(),
)
is_private = fields.Boolean(
string="Private",
help="Repository is private",
related="repo_id.is_private",
store=True,
readonly=True,
)
head_type = fields.Selection(
selection=[
("branch", "Branch"),
("pr", "Pull/Merge Request"),
("commit", "Commit"),
],
required=True,
)
head = fields.Char(
help="Git remote head. Link to branch, PR, commit or commit hash.",
required=True,
index=True,
)
def _get_default_url_protocol(self):
"""Default URL protocol for new remote.
Returns:
Char: Default URL protocol.
"""
return "https"
@api.depends("source_id", "sequence")
def _compute_name(self):
"""
Compute remote name.
By default all remotes are named `remote_<position>`
where position is the position of the remote in the source.
Eg first remote is `remote_1`, second is `remote_2`, etc.
"""
for remote in self:
if remote.source_id:
for index, source_remote in enumerate(remote.source_id.remote_ids):
source_remote.name = f"remote_{index + 1}"
@api.onchange("head")
def onchange_head(self):
"""
Extract head number from head url
and set it as head.
"""
for remote in self:
if remote.head and "/" in remote.head:
remote.head = self._sanitize_head(remote.head)
@api.model_create_multi
def create(self, vals_list):
# Sanitize head
for vals in vals_list:
head = vals.get("head")
if head and "/" in head:
vals["head"] = self._sanitize_head(head)
res = super().create(vals_list)
# Export project to related files and templates
res._update_related_files_and_templates()
return res
def write(self, vals):
# Sanitize head
if "head" in vals:
head = vals["head"]
if head and "/" in head:
vals["head"] = self._sanitize_head(head)
res = super().write(vals)
# Update related files and templates on update
self._update_related_files_and_templates()
return res
def unlink(self):
"""
Override to update related files and templates on unlink
"""
projects = self.git_project_id
res = super().unlink()
# Update related files and templates on unlink
if projects:
file_relations = projects.git_project_rel_ids # type: ignore
if file_relations:
file_relations._save_to_file()
template_relations = projects.git_project_file_template_rel_ids # type: ignore
if template_relations:
template_relations._save_to_file_template()
return res
def _sanitize_head(self, head):
"""Sanitize head.
Extract head number from head url
and set it as head.
Args:
head (Char): Head to sanitize
Returns:
Char: Sanitized head
"""
if head and "/" in head:
return head.split("/")[-1].strip()
return head
def _update_related_files_and_templates(self):
# Update related files on update
related_files = self.mapped("git_project_id").mapped("git_project_rel_ids")
if related_files:
related_files._save_to_file()
related_templates = self.mapped("git_project_id").mapped(
"git_project_file_template_rel_ids"
)
if related_templates:
related_templates._save_to_file_template()
# ------------------------------
# Reference mixin methods
# ------------------------------
def _get_pre_populated_model_data(self):
res = super()._get_pre_populated_model_data()
res.update({"cx.tower.git.remote": ["cx.tower.git.source", "source_id"]})
return res
# ------------------------------
# YAML mixin methods
# ------------------------------
def _get_fields_for_yaml(self):
res = super()._get_fields_for_yaml()
res += [
"name",
"enabled",
"sequence",
"repo_id",
"head",
"head_type",
]
return res
# ------------------------------
# Git Aggregator related methods
# ------------------------------
def _git_aggregator_prepare_url(self):
"""Prepare url for git aggregator
Returns:
Char: Prepared url for git aggregator
"""
self.ensure_one()
if not self.repo_id:
raise ValidationError(_("Repository is required"))
if not self.repo_id.url:
raise ValidationError(_("Repository URL is not set"))
url = self.repo_id.url
prepared_url = giturlparse.parse(url).urls.get(self.url_protocol, url)
# If repo is public or is not using HTTPS protocol return URL as is
if not self.is_private or self.url_protocol != "https":
return prepared_url
if self.repo_provider == "github":
prepared_url = self._git_aggregator_prepare_url_github(prepared_url)
elif self.repo_provider == "gitlab":
prepared_url = self._git_aggregator_prepare_url_gitlab(prepared_url)
elif self.repo_provider == "bitbucket":
prepared_url = self._git_aggregator_prepare_url_bitbucket(prepared_url)
return prepared_url
def _git_aggregator_prepare_authenticated_url(self, url, auth_token):
"""Helper to inject authentication token into HTTPS URL.
Args:
url (Char): URL to prepare
auth_token (Char): Authentication token
Returns:
Char: Prepared url for git aggregator
"""
url_without_protocol = url.replace("https://", "")
return f"https://{auth_token}@{url_without_protocol}"
def _git_aggregator_prepare_url_github(self, url):
"""
Prepare url for git aggregator for private Github repo
using https protocol.
Args:
url (Char): URL to prepare
Returns:
Char: Prepared url for git aggregator
"""
self.ensure_one()
return self._git_aggregator_prepare_authenticated_url(
url,
"$GITHUB_TOKEN:x-oauth-basic",
)
def _git_aggregator_prepare_url_gitlab(self, url):
"""
Prepare url for git aggregator for private GitLab repo
using https protocol.
Args:
url (Char): URL to prepare
Returns:
Char: Prepared url for git aggregator
"""
self.ensure_one()
return self._git_aggregator_prepare_authenticated_url(
url, "$GITLAB_TOKEN_NAME:$GITLAB_TOKEN"
)
def _git_aggregator_prepare_url_bitbucket(self, url):
"""
Prepare url for git aggregator for private Bitbucket repo
using https protocol.
Args:
url (Char): URL to prepare
Returns:
Char: Prepared url for git aggregator
"""
self.ensure_one()
return self._git_aggregator_prepare_authenticated_url(
url, "x-token-auth:$BITBUCKET_TOKEN"
)
def _git_aggregator_prepare_head(self):
"""Prepare head for git aggregator
Returns:
Char: Prepared head for git aggregator
"""
self.ensure_one()
if self.repo_provider == "github":
return self._git_aggregator_prepare_head_github()
if self.repo_provider == "gitlab":
return self._git_aggregator_prepare_head_gitlab()
if self.repo_provider == "bitbucket":
return self._git_aggregator_prepare_head_bitbucket()
return self.head
def _extract_head_number(self):
"""
Extract the last component from head
(branch name, PR number, or commit hash).
Raises:
ValidationError: If head number is empty
Returns:
Char: Extracted head number
"""
self.ensure_one()
head_number = self.head.split("/")[-1]
if not head_number:
raise ValidationError(
_("Git Aggregator: Head number is empty in %(head)s", head=self.head)
)
return head_number
def _git_aggregator_prepare_head_github(self):
"""Prepare head for git aggregator for Github.
Returns:
Char: Prepared head for git aggregator
"""
self.ensure_one()
head_number = self._extract_head_number()
# PR/MR
if self.head_type == "pr":
return f"refs/pull/{head_number}/head"
# Commit
if self.head_type in ["commit", "branch"]:
return f"{head_number}"
# Fallback to original head
return self.head
def _git_aggregator_prepare_head_gitlab(self):
"""Prepare head for git aggregator for GitLab.
Returns:
Char: Prepared head for git aggregator
"""
self.ensure_one()
head_number = self._extract_head_number()
# PR/MR
if self.head_type == "pr":
return f"merge-requests/{head_number}/head"
# Commit
# https://gitlab.com/cetmix/test/-/list/17.0-test-branch?ref_type=heads
if self.head_type in ["commit", "branch"]:
head_parts = head_number.split("?")
return f"{head_parts[0]}"
# Fallback to original head
return self.head
def _git_aggregator_prepare_head_bitbucket(self):
"""Prepare head for git aggregator for Bitbucket.
Returns:
Char: Prepared head for git aggregator
"""
self.ensure_one()
# PR/MR
if self.head_type == "pr":
raise ValidationError(
_(
"Git Aggregator: "
"Bitbucket does not support"
" fetching PRs. Please use branch instead.\n\n"
"Source: %(src)s\n"
"URL: %(url)s\n"
"Head: %(head)s",
src=self.source_id.name,
url=self.repo_id.url,
head=self.head,
)
)
head_number = self._extract_head_number()
# Commit
if self.head_type in ["commit", "branch"]:
return f"{head_number}"
# Fallback to original head
return self.head

View File

@@ -0,0 +1,416 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
import giturlparse
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import ormcache
_logger = logging.getLogger(__name__)
class CxTowerGitRepo(models.Model):
"""
Git Repository.
Represents a git repository with its metadata and configuration.
"""
_name = "cx.tower.git.repo"
_inherit = [
"cx.tower.reference.mixin",
"cx.tower.yaml.mixin",
]
_description = "Cetmix Tower Git Repository"
_order = "name"
_rec_names_search = ["repo", "host", "owner_id"]
active = fields.Boolean(default=True, help="Indicates if the repository is active")
name = fields.Char(
compute="_compute_name", store=True, required=False, index="trigram"
)
reference = fields.Char(
index=True,
compute="_compute_name",
required=False,
store=True,
)
repo = fields.Char(
string="Repository Name",
readonly=True,
help="Repository name (e.g., 'cetmix-tower', 'odoo')",
)
url = fields.Char(
string="Generic URL",
help="Displayed in 'https' format, but can be entered in any format",
compute="_compute_url",
inverse="_inverse_url",
required=True,
compute_sudo=True,
)
url_ssh = fields.Char(
string="SSH URL",
help="SSH URL of the repository",
compute="_compute_url",
compute_sudo=True,
)
url_git = fields.Char(
string="GIT URL",
help="GIT URL of the repository",
compute="_compute_url",
compute_sudo=True,
)
is_private = fields.Boolean(
string="Private", default=False, help="Indicates if the repository is private"
)
provider = fields.Selection(
selection="_selection_provider",
required=True,
default="other",
help="Repository provider to determine provider-based behaviour",
)
host = fields.Char(
readonly=True,
index=True,
help="Repository host (e.g., 'github.com', 'gitlab.com')",
)
owner_id = fields.Many2one(
comodel_name="cx.tower.git.repo.owner",
readonly=True,
help="Repository owner (e.g., 'cetmix' or 'OCA')",
)
secret_id = fields.Many2one(
comodel_name="cx.tower.key",
string="Secret",
domain="[('key_type', '=', 's')]",
help="Custom secret used for this repository",
)
remote_ids = fields.One2many(
comodel_name="cx.tower.git.remote",
inverse_name="repo_id",
help="Remotes that use this repository",
)
git_project_ids = fields.Many2many(
comodel_name="cx.tower.git.project",
relation="cx_tower_git_repo_project_rel",
column1="repo_id",
column2="project_id",
compute="_compute_git_project_ids",
store=True,
help="Projects this repository is used in",
)
remote_count = fields.Integer(
compute="_compute_remote_count",
help="Number of remotes this repository is used in",
)
git_project_count = fields.Integer(
compute="_compute_git_project_count",
help="Number of projects this repository is used in",
)
_sql_constraints = [
(
"unique_repo_host_owner",
"unique(repo, host, owner_id)",
"A repository with the same name, host, and owner already exists.",
),
]
# -- Selection
def _selection_provider(self):
"""Available repository providers.
Returns:
List of tuples: available options.
"""
return [
("github", "GitHub"),
("gitlab", "GitLab"),
("bitbucket", "Bitbucket"),
("assembla", "Assembla"),
("other", "Other"),
]
# -- Computes
@api.depends("host", "owner_id", "owner_id.name", "repo")
def _compute_name(self):
"""
Compute name in format: host/owner/name.
Compute reference based on name.
"""
for repo in self:
if repo.host and repo.owner_id and repo.repo:
name = f"{repo.host}/{repo.owner_id.name}/{repo.repo}"
reference = repo._generate_or_fix_reference(name)
repo.update(
{
"name": name,
"reference": reference,
}
)
else:
repo.update(
{
"name": False,
"reference": False,
}
)
@api.depends("remote_ids", "remote_ids.git_project_id")
def _compute_git_project_ids(self):
"""Compute projects this repository is used in."""
for repo in self:
repo.git_project_ids = repo.remote_ids.git_project_id
@api.depends("remote_ids")
def _compute_remote_count(self):
"""Compute remote count field."""
for repo in self:
repo.remote_count = len(repo.remote_ids)
@api.depends("git_project_ids")
def _compute_git_project_count(self):
"""Compute project count field."""
for repo in self:
repo.git_project_count = len(repo.git_project_ids)
@api.depends("repo", "host", "owner_id")
def _compute_url(self):
"""Compute URL from repository properties."""
for repo in self:
if repo.repo and repo.host and repo.owner_id:
https_url = f"https://{repo.host}/{repo.owner_id.name}/{repo.repo}.git"
elif repo.repo and repo.host:
https_url = f"https://{repo.host}/{repo.repo}.git"
else:
https_url = ""
if https_url:
try:
parsed_urls = giturlparse.parse(https_url).urls
urls = {
"url": https_url,
"url_ssh": parsed_urls["ssh"],
"url_git": parsed_urls["git"],
}
except Exception as e: # noqa: F841 catch all errors
_logger.error(
"Failed to parse constructed URL '%s' for repo %s",
https_url,
repo.display_name,
)
urls = {
"url": "",
"url_ssh": "",
"url_git": "",
}
else:
urls = {
"url": "",
"url_ssh": "",
"url_git": "",
}
repo.update(urls)
def _inverse_url(self):
"""Parse URL to update repository properties."""
for repo in self:
if not repo.url:
continue
# Parse URL
parsed_url_dict = self._parse_url(repo.url)
# Update repository properties
repo.update(parsed_url_dict)
def action_view_remotes(self):
"""Open remotes list view."""
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id(
"cetmix_tower_git.action_cx_tower_git_remote"
)
action.update(
{
"domain": [("repo_id", "=", self.id)],
"context": {"default_repo_id": self.id},
}
)
return action
def action_view_projects(self):
"""Open projects list view."""
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id(
"cetmix_tower_git.cx_tower_git_project_action"
)
action.update(
{
"domain": [("repo_ids", "in", self.id)],
"context": {"default_repo_ids": [(4, self.id)]},
}
)
return action
@api.model_create_multi
def create(self, vals_list):
"""Create multiple repositories."""
# Check if any of the repositories already exist
# This is needed to allow creating repositories using just an URL.
# Eg when importing repositories from a YAML file.
res = self.browse()
existing_repo_ids = []
vals_list_to_create = []
for vals in vals_list:
url = vals.get("url")
if url:
# Try to get repository by URL
repo_id = self._get_repo_id_by_url(
url=url, create=False, raise_if_invalid=False
)
if repo_id:
existing_repo_ids.append(repo_id)
continue
# Parse URL and update vals
parsed_url_dict = self._parse_url(url=url, raise_if_invalid=True)
vals.update(parsed_url_dict)
# Add to create list (with or without URL)
vals_list_to_create.append(vals)
# Compose the result
if vals_list_to_create:
res |= super().create(vals_list_to_create)
if existing_repo_ids:
res |= self.browse(existing_repo_ids)
self.env.registry.clear_cache()
return res
def write(self, vals):
"""Write repositories."""
res = super().write(vals)
self.env.registry.clear_cache()
return res
def unlink(self):
"""Unlink repositories."""
res = super().unlink()
self.env.registry.clear_cache()
return res
@api.model
def name_create(self, name):
"""
Create a new repository from a URL.
"""
repo_id = self._get_repo_id_by_url(url=name, create=True, raise_if_invalid=True)
repo = self.browse(repo_id)
return repo_id, repo.display_name
@ormcache("self.env.uid", "self.env.su", "url", "create", "raise_if_invalid")
def _get_repo_id_by_url(self, url, create=False, raise_if_invalid=False):
"""Get repository id by URL.
Args:
url (Char): URL to get repository id
create (Bool, optional): Create repository if not found.
Default is False.
raise_if_invalid (Bool, optional): Raise ValidationError
if the URL is not valid. Default is False.
Returns:
int: Repository ID
or False if the URL is not valid and raise_if_invalid is False
Raises:
ValidationError: If the URL is not valid and raise_if_invalid is True
"""
# Parse URL
parsed_url_dict = self._parse_url(url, raise_if_invalid=raise_if_invalid)
if not parsed_url_dict:
return False
# Check if repository already exists and use it
repo = self.search(
[
("repo", "=", parsed_url_dict["repo"]),
("host", "=", parsed_url_dict["host"]),
("owner_id", "=", parsed_url_dict["owner_id"]),
],
limit=1,
)
# Otherwise, create a new one
if not repo and create:
repo = self.create(parsed_url_dict)
return repo.id if repo else False
def _parse_url(self, url, raise_if_invalid=True):
"""Parse URL to get name, host and owner.
Args:
url (Char): URL to parse
Raises:
ValidationError: If the URL is not valid
Returns:
Dict: Dictionary with name, host and owner
or empty dict if the URL is not valid and raise_if_invalid is False
"""
# Validate URL
if not giturlparse.validate(url):
if raise_if_invalid:
raise ValidationError(_("Not a valid repository URL!"))
return {}
# Parse URL
parsed_url = giturlparse.parse(url)
# Get or create owner
owner_id = self.env["cx.tower.git.repo.owner"]._get_owner_id_by_name(
name=parsed_url.owner,
create=True,
)
# Get provider based on host
provider = self._get_provider(parsed_url)
return {
"repo": parsed_url.repo,
"host": parsed_url.host,
"owner_id": owner_id,
"provider": provider,
}
def _get_provider(self, parsed_url):
"""Get provider.
Args:
parsed_url (GitUrlParsed): Parsed URL object
Returns:
str: Provider name
"""
provider = "other"
if parsed_url.assembla:
provider = "assembla"
elif parsed_url.bitbucket or "bitbucket" in parsed_url.host:
provider = "bitbucket"
elif parsed_url.gitlab:
provider = "gitlab"
elif parsed_url.github:
provider = "github"
return provider
# ------------------------------
# YAML mixin methods
# ------------------------------
def _get_fields_for_yaml(self):
res = super()._get_fields_for_yaml()
res += [
"url",
"is_private",
"secret_id",
]
return res

View File

@@ -0,0 +1,107 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo.tools import ormcache
class CxTowerGitRepoOwner(models.Model):
"""
Git Repository Owner.
Represents an organization or user that owns repositories.
Examples: "cetmix", "OCA", etc.
"""
_name = "cx.tower.git.repo.owner"
_inherit = ["cx.tower.reference.mixin", "cx.tower.yaml.mixin"]
_description = "Cetmix Tower Git Repository Owner"
_order = "name"
display_name = fields.Char(
readonly=False, compute="_compute_display_name", store=True
)
name = fields.Char(
help="Name of the repository owner (e.g., 'cetmix', 'OCA')",
)
reference = fields.Char(
index=True,
compute="_compute_display_name",
required=False,
store=True,
)
repo_ids = fields.One2many(
comodel_name="cx.tower.git.repo",
inverse_name="owner_id",
string="Repositories",
copy=False,
help="Repositories owned by this organization/user",
)
secret_id = fields.Many2one(
comodel_name="cx.tower.key",
string="Secret",
domain="[('key_type', '=', 's')]",
help="Custom secret used for this repository owner",
)
@api.depends("name")
def _compute_display_name(self):
"""Compute display name."""
for owner in self:
# By default, display name is the same as name
name = owner.name
owner.update(
{
"display_name": name or False,
"reference": owner._generate_or_fix_reference(name)
if name
else False,
}
)
@ormcache("self.env.uid", "self.env.su", "name", "create")
def _get_owner_id_by_name(self, name, create=False):
"""Get owner id by name.
Args:
name (str): Owner name
create (bool): Create owner if not found
Returns:
int: Owner ID or None if not found
"""
owner = self.search([("name", "=ilike", name)], limit=1) if name else None
if not owner and create and name:
owner = self.create({"name": name})
return owner.id if owner else None
@api.model_create_multi
def create(self, vals_list):
"""Clear cache on create."""
res = super().create(vals_list)
self.env.registry.clear_cache()
return res
def write(self, vals):
"""Clear cache on write."""
res = super().write(vals)
if "name" in vals:
self.env.registry.clear_cache()
return res
def unlink(self):
"""Clear cache on unlink."""
res = super().unlink()
self.env.registry.clear_cache()
return res
# ------------------------------
# YAML mixin methods
# ------------------------------
def _get_fields_for_yaml(self):
res = super()._get_fields_for_yaml()
res += [
"display_name",
"name",
"secret_id",
]
return res

View File

@@ -0,0 +1,195 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class CxTowerGitSource(models.Model):
"""
Git Source.
Implements single git source.
Each source can include multiple remotes which can be
branches or pull requests of different repositories.
"""
_name = "cx.tower.git.source"
_description = "Cetmix Tower Git Source"
_inherit = [
"cx.tower.reference.mixin",
"cx.tower.yaml.mixin",
]
_order = "sequence, name"
active = fields.Boolean(related="git_project_id.active", store=True, readonly=True)
enabled = fields.Boolean(
default=True, help="Enable in configuration and exported to files"
)
name = fields.Char(required=False)
sequence = fields.Integer(default=10)
git_project_id = fields.Many2one(
comodel_name="cx.tower.git.project",
string="Git Configuration",
required=True,
ondelete="cascade",
auto_join=True,
)
remote_ids = fields.One2many(
comodel_name="cx.tower.git.remote",
inverse_name="source_id",
auto_join=True,
copy=True,
)
remote_count = fields.Integer(
compute="_compute_remote_count",
string="Remotes",
)
remote_count_private = fields.Integer(
compute="_compute_remote_count",
string="Private Remotes",
)
@api.depends("remote_ids", "remote_ids.enabled", "remote_ids.is_private")
def _compute_remote_count(self):
for record in self:
remote_count = private_remote_count = 0
for remote in record.remote_ids:
if not remote.enabled:
continue
if remote.is_private:
private_remote_count += 1
remote_count += 1
record.update(
{
"remote_count": remote_count,
"remote_count_private": private_remote_count,
}
)
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
# Update name
no_name = res.filtered(lambda s: not s.name)
if no_name:
no_name._compose_name()
# Update related files and templates on create
res._update_related_files_and_templates()
return res
def write(self, vals):
res = super().write(vals)
# Compose name
if "name" in vals and not vals.get("name"):
self._compose_name()
# Update related files and templates on update
self._update_related_files_and_templates()
return res
def unlink(self):
"""
Override to update related files and templates on unlink
"""
projects = self.git_project_id
res = super().unlink()
# Update related files and templates on unlink
if projects:
file_relations = projects.git_project_rel_ids # type: ignore
if file_relations:
file_relations._save_to_file()
template_relations = projects.git_project_file_template_rel_ids # type: ignore
if template_relations:
template_relations._save_to_file_template()
return res
def _compose_name(self):
"""Compose name if not provided explicitly"""
for source in self:
if source.name:
continue
remote = fields.first(source.remote_ids)
if not remote:
source.name = "Empty Source"
continue
remote_repo = remote.repo_id
if not remote_repo or not remote_repo.owner_id:
source.name = "Empty Source"
continue
source.name = f"{remote_repo.owner_id.name}/{remote_repo.repo}"
def _update_related_files_and_templates(self):
# Update related files and templates on update
related_files = self.mapped("git_project_id").mapped("git_project_rel_ids")
if related_files:
related_files._save_to_file()
related_templates = self.mapped("git_project_id").mapped(
"git_project_file_template_rel_ids"
)
if related_templates:
related_templates._save_to_file_template()
# ------------------------------
# Reference mixin methods
# ------------------------------
def _get_pre_populated_model_data(self):
res = super()._get_pre_populated_model_data()
res.update({"cx.tower.git.source": ["cx.tower.git.project", "git_project_id"]})
return res
# ------------------------------
# YAML mixin methods
# ------------------------------
def _get_fields_for_yaml(self):
res = super()._get_fields_for_yaml()
res += [
"name",
"enabled",
"sequence",
"remote_ids",
]
return res
# ------------------------------
# Git Aggregator related methods
# ------------------------------
def _git_aggregator_prepare_record(self):
"""Prepare json structure for git aggregator.
Returns:
Dict: Json structure for git aggregator
"""
self.ensure_one()
# Prepare remotes, merges and target
remotes = {}
merges = []
target = None
for remote in self.remote_ids:
if remote.enabled:
remotes.update({remote.name: remote._git_aggregator_prepare_url()})
merges.append(
{
"remote": remote.name,
"ref": remote._git_aggregator_prepare_head(),
}
)
# Set target to first remote name
if not target:
target = remote.name
# If no remotes, return empty dict
if not remotes:
return {}
vals = {
"remotes": remotes,
"merges": merges,
"target": target,
}
# Fetch only first commit if there is only one remote
if len(remotes) == 1:
vals.update({"defaults": {"depth": 1}})
return vals

View File

@@ -0,0 +1,32 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class CxTowerPlanLine(models.Model):
"""Flight Plan Line"""
_inherit = "cx.tower.plan.line"
git_project_id = fields.Many2one(
comodel_name="cx.tower.git.project",
string="Git Project",
help="Select a git project to be linked to the file and server.",
)
is_make_copy = fields.Boolean(
string="Make a Copy",
help="Create a copy of the Git Project instead of linking "
"the file to the existing one.",
)
# ------------------------------
# YAML mixin methods
# ------------------------------
def _get_fields_for_yaml(self):
res = super()._get_fields_for_yaml()
res += [
"git_project_id",
"is_make_copy",
]
return res

View File

@@ -0,0 +1,187 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class CxTowerServer(models.Model):
_inherit = "cx.tower.server"
git_project_rel_ids = fields.One2many(
comodel_name="cx.tower.git.project.rel",
inverse_name="server_id",
copy=False,
depends=["git_project_ids"],
groups="cetmix_tower_server.group_manager,cetmix_tower_server.group_root",
)
# Helper field to get all git projects related to server
# IMPORTANT: This field may contain duplicates because of the relation nature!
git_project_ids = fields.Many2many(
comodel_name="cx.tower.git.project",
relation="cx_tower_git_project_rel",
column1="server_id",
column2="git_project_id",
readonly=True,
copy=False,
depends=["git_project_rel_ids"],
groups="cetmix_tower_server.group_manager,cetmix_tower_server.group_root",
)
# ------------------------------
# YAML mixin methods
# ------------------------------
def _get_fields_for_yaml(self):
res = super()._get_fields_for_yaml()
res += [
"git_project_rel_ids",
]
return res
def _get_force_x2m_resolve_models(self):
res = super()._get_force_x2m_resolve_models()
# Add File in order to always try to use existing one
res += ["cx.tower.file"]
return res
def _update_or_create_related_record(
self, model, reference, values, create_immediately=False
):
# Files must be created immediately because they are related
# to both server and git project.
# So if a file is not created immediately when it is created
# for the server, the same file will be created for the git project.
# This will lead to creation of two files with the same content
# for the same server.
if model._name == "cx.tower.file":
create_immediately = True
return super()._update_or_create_related_record(
model, reference, values, create_immediately=create_immediately
)
@api.model
def get_servers_by_git_ref(self, repository_url, head=None, head_type=None):
"""
Return servers linked to a given Git repository reference.
Parameters
----------
repository_url : str
Pre-normalized canonical Git URL
(e.g. ``https://host/owner/repo.git``).
head : str, optional
Branch name, commit SHA, or PR identifier.
head_type : {'branch', 'commit', 'pr'}, optional
Type of the ``head`` argument.
If only ``head`` is provided, it will match across all head types.
If only ``head_type`` is provided, it will filter by type regardless of head
Returns
-------
recordset of cx.tower.server
Matching servers. Empty recordset if no matches.
"""
server_obj = self.env["cx.tower.server"]
# URL MUST be already canonical.
if not repository_url:
return server_obj
# Get repository id by URL
repo_id = self.env["cx.tower.git.repo"]._get_repo_id_by_url(
repository_url, raise_if_invalid=False
)
if not repo_id:
return server_obj
repo = self.env["cx.tower.git.repo"].browse(repo_id)
# Compose domain for remotes
remote_domain = [
("source_id.enabled", "=", True),
("enabled", "=", True),
]
if head:
head = self.env["cx.tower.git.remote"]._sanitize_head(head)
remote_domain.append(("head", "=", head))
if head_type:
remote_domain.append(("head_type", "=", head_type))
# Get remotes
remotes = repo.remote_ids.filtered_domain(remote_domain)
if not remotes:
return server_obj
# Get servers from remotes
servers = remotes.mapped("git_project_id.git_project_rel_ids.server_id")
return servers
def _command_runner_file_using_template_create_file(
self,
log_record,
server_dir,
**kwargs,
):
"""Override to create git project relation
when creating a file using a template.
"""
file = super()._command_runner_file_using_template_create_file(
log_record, server_dir, **kwargs
)
if file:
# Get the flight plan line from log record
plan_line = log_record.plan_log_id.plan_line_executed_id
# Try to get git project from custom values
custom_values = log_record.variable_values
git_project_reference = custom_values and custom_values.get(
"__git_project__"
)
if git_project_reference:
git_project = self.env["cx.tower.git.project"].get_by_reference(
git_project_reference
)
if not git_project:
_logger.warning(
"Git project '%s' provided with the `__git_project__` "
"custom value not found for server '%s' "
"in flight plan line '%s' "
"of the flight plan '%s'. "
"No project was linked to the file '%s'.",
git_project_reference,
self.name,
plan_line.name,
log_record.plan_log_id.plan_id.name,
file.name,
)
# Try to get git project set explicitly in the flight plan line
else:
git_project = plan_line.git_project_id
if not git_project:
return file
if plan_line.is_make_copy:
# Remove default_server_ids from context, because this relation
# will be created through git_project_rel_ids.
# default_server_ids will interfere at the moment when
# pairs of values are created through SQL query
# in the method write_real and it does not take into account
# that in this case we are creating a copy of the git project
git_project = git_project.with_context(default_server_ids=False).copy(
{"name": git_project._compose_copy_name(server=self)}
)
self.env["cx.tower.git.project.rel"].create(
{
"git_project_id": git_project.id,
"server_id": self.id,
"file_id": file.id,
"project_format": git_project._default_project_format(),
}
)
return file

View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1 @@
Please refer to the [official documentation](https://cetmix.com/tower) for detailed configuration instructions.

View File

@@ -0,0 +1,3 @@
This module implements Git Management functionality for [Cetmix Tower](https://cetmix.com/tower).
Please refer to the [official documentation](https://cetmix.com/tower) for detailed information.

View File

@@ -0,0 +1,10 @@
## 18.0.1.0.2 (2026-03-10)
- Features: Provide git project name using the `__git_project__` custom value when creating a project in flight plan. Improve the UI and UX of Git Projects. (5197)
- Bugfixes: Link server to git project only once. (5214)
## 18.0.1.0.1 (2025-12-17)
- Features: Improve search views, implement the search panel for selected views. (5139)

View File

@@ -0,0 +1 @@
Please refer to the [official documentation](https://cetmix.com/tower) for detailed usage instructions.

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Manager Read Rule -->
<record id="rule_git_project_file_template_rel_manager_read" model="ir.rule">
<field
name="name"
>Git Project File Template Relation: Manager Read Access</field>
<field name="model_id" ref="model_cx_tower_git_project_file_template_rel" />
<field name="domain_force">['&amp;',
'|',
('git_project_id.user_ids', 'in', [user.id]),
('git_project_id.manager_ids', 'in', [user.id]),
'|',
('file_template_id.user_ids', 'in', [user.id]),
('file_template_id.manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="1" />
<field name="perm_write" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Manager Write/Create/Delete Rule -->
<record id="rule_git_project_file_template_rel_manager_write" model="ir.rule">
<field
name="name"
>Git Project File Template Relation: Manager Write Access</field>
<field name="model_id" ref="model_cx_tower_git_project_file_template_rel" />
<field name="domain_force">[
('git_project_id.manager_ids', 'in', [user.id]),
('file_template_id.manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="0" />
<field name="perm_write" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_unlink" eval="1" />
</record>
<!-- Root Access Rule -->
<record id="rule_git_project_file_template_rel_root" model="ir.rule">
<field name="name">Git Project File Template Relation: Root Full Access</field>
<field name="model_id" ref="model_cx_tower_git_project_file_template_rel" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
</record>
</odoo>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Manager Read Rule -->
<record id="rule_git_project_rel_manager_read" model="ir.rule">
<field name="name">Git Project Relation: Manager Read Access</field>
<field name="model_id" ref="model_cx_tower_git_project_rel" />
<field name="domain_force">['&amp;',
'|',
('git_project_id.user_ids', 'in', [user.id]),
('git_project_id.manager_ids', 'in', [user.id]),
'|',
('server_id.user_ids', 'in', [user.id]),
('server_id.manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="1" />
<field name="perm_write" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Manager Write/Create/Delete Rule -->
<record id="rule_git_project_rel_manager_create_write_unlink" model="ir.rule">
<field
name="name"
>Git Project Relation: Manager Create/Write/Delete Access</field>
<field name="model_id" ref="model_cx_tower_git_project_rel" />
<field name="domain_force">[('git_project_id.manager_ids', 'in', [user.id]),
('server_id.manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="0" />
<field name="perm_write" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_unlink" eval="1" />
</record>
<!-- Root Access Rule -->
<record id="rule_git_project_rel_root" model="ir.rule">
<field name="name">Git Project Relation: Root Full Access</field>
<field name="model_id" ref="model_cx_tower_git_project_rel" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
</record>
</odoo>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Manager Read Rule -->
<record id="rule_git_project_manager_read" model="ir.rule">
<field name="name">Git Project: Manager Read Access</field>
<field name="model_id" ref="model_cx_tower_git_project" />
<field
name="domain_force"
>['|', ('user_ids', 'in', [user.id]), ('manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="1" />
<field name="perm_write" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Additional Manager Read Rule with Server Access -->
<record id="rule_git_project_manager_read_server" model="ir.rule">
<field name="name">Git Project: Manager Read Access via Server</field>
<field name="model_id" ref="model_cx_tower_git_project" />
<field name="domain_force">['|',
('git_project_rel_ids.server_id.user_ids', 'in', [user.id]),
('git_project_rel_ids.server_id.manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="1" />
<field name="perm_write" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Manager Write/Create Rule -->
<record id="rule_git_project_manager_write" model="ir.rule">
<field name="name">Git Project: Manager Write Access</field>
<field name="model_id" ref="model_cx_tower_git_project" />
<field name="domain_force">[('manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="0" />
<field name="perm_write" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Manager Delete Rule -->
<record id="rule_git_project_manager_unlink" model="ir.rule">
<field name="name">Git Project: Manager Delete Access</field>
<field name="model_id" ref="model_cx_tower_git_project" />
<field
name="domain_force"
>[('manager_ids', 'in', [user.id]), ('create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_unlink" eval="1" />
</record>
<!-- Root Access Rule -->
<record id="rule_git_project_root" model="ir.rule">
<field name="name">Git Project: Root Full Access</field>
<field name="model_id" ref="model_cx_tower_git_project" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
</record>
</odoo>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Manager Read Rule -->
<record id="rule_git_remote_manager_read" model="ir.rule">
<field name="name">Git Remote: Manager Read Access</field>
<field name="model_id" ref="model_cx_tower_git_remote" />
<field
name="domain_force"
>['|', ('git_project_id.user_ids', 'in', [user.id]), ('git_project_id.manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="1" />
<field name="perm_write" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Additional Manager Read Rule with Server Access -->
<record id="rule_git_remote_manager_read_server" model="ir.rule">
<field name="name">Git Remote: Manager Read Access via Server</field>
<field name="model_id" ref="model_cx_tower_git_remote" />
<field name="domain_force">['|',
('git_project_id.git_project_rel_ids.server_id.user_ids', 'in', [user.id]),
('git_project_id.git_project_rel_ids.server_id.manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="1" />
<field name="perm_write" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Manager Write/Create Rule -->
<record id="rule_git_remote_manager_write" model="ir.rule">
<field name="name">Git Remote: Manager Write/Create Access</field>
<field name="model_id" ref="model_cx_tower_git_remote" />
<field
name="domain_force"
>[('git_project_id.manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="0" />
<field name="perm_write" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Manager Delete Rule -->
<record id="rule_git_remote_manager_unlink" model="ir.rule">
<field name="name">Git Remote: Manager Delete Access</field>
<field name="model_id" ref="model_cx_tower_git_remote" />
<field
name="domain_force"
>[('git_project_id.manager_ids', 'in', [user.id]), ('create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_unlink" eval="1" />
</record>
<!-- Root Access Rule -->
<record id="rule_git_remote_root" model="ir.rule">
<field name="name">Git Remote: Root Full Access</field>
<field name="model_id" ref="model_cx_tower_git_remote" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
<field name="perm_read" eval="1" />
<field name="perm_write" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_unlink" eval="1" />
</record>
</odoo>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Manager Read Rule - View All Records - nothing to add as this is default -->
<!-- Manager Write/Create Rule -->
<record id="rule_git_repo_owner_manager_write" model="ir.rule">
<field name="name">Git Repository Owner: Manager Write/Create Access</field>
<field name="model_id" ref="model_cx_tower_git_repo_owner" />
<field name="domain_force">[('create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="0" />
<field name="perm_write" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Root Access Rule -->
<record id="rule_git_repo_owner_root" model="ir.rule">
<field name="name">Git Repository Owner: Root Full Access</field>
<field name="model_id" ref="model_cx_tower_git_repo_owner" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
</record>
</odoo>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Manager Read Rule - View All Records - nothing to add as this is default -->
<!-- Manager Write/Create Rule -->
<record id="rule_git_repo_manager_write" model="ir.rule">
<field name="name">Git Repository: Manager Write/Create Access</field>
<field name="model_id" ref="model_cx_tower_git_repo" />
<field name="domain_force">[('create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="0" />
<field name="perm_write" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Root Access Rule -->
<record id="rule_git_repo_root" model="ir.rule">
<field name="name">Git Repository: Root Full Access</field>
<field name="model_id" ref="model_cx_tower_git_repo" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
</record>
</odoo>

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Git Source Record Rules -->
<!-- Manager Read Rule -->
<record id="rule_git_source_manager_read" model="ir.rule">
<field name="name">Git Source: Manager Read Access</field>
<field name="model_id" ref="model_cx_tower_git_source" />
<field
name="domain_force"
>['|', ('git_project_id.user_ids', 'in', [user.id]), ('git_project_id.manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="1" />
<field name="perm_write" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Additional Manager Read Rule with Server Access -->
<record id="rule_git_source_manager_read_server" model="ir.rule">
<field name="name">Git Source: Manager Read Access via Server</field>
<field name="model_id" ref="model_cx_tower_git_source" />
<field name="domain_force">['|',
('git_project_id.git_project_rel_ids.server_id.user_ids', 'in', [user.id]),
('git_project_id.git_project_rel_ids.server_id.manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="1" />
<field name="perm_write" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Manager Write/Create Rule -->
<record id="rule_git_source_manager_write" model="ir.rule">
<field name="name">Git Source: Manager Write/Create Access</field>
<field name="model_id" ref="model_cx_tower_git_source" />
<field
name="domain_force"
>[('git_project_id.manager_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="0" />
<field name="perm_write" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_unlink" eval="0" />
</record>
<!-- Manager Delete Rule -->
<record id="rule_git_source_manager_unlink" model="ir.rule">
<field name="name">Git Source: Manager Delete Access</field>
<field name="model_id" ref="model_cx_tower_git_source" />
<field
name="domain_force"
>[('git_project_id.manager_ids', 'in', [user.id]), ('create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
<field name="perm_read" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_unlink" eval="1" />
</record>
<!-- Root Access Rule -->
<record id="rule_git_source_root" model="ir.rule">
<field name="name">Git Source: Root Full Access</field>
<field name="model_id" ref="model_cx_tower_git_source" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
<field name="perm_read" eval="1" />
<field name="perm_write" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_unlink" eval="1" />
</record>
</odoo>

View File

@@ -0,0 +1,15 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_git_config_manager,Git Config Manager,model_cx_tower_git_project,cetmix_tower_server.group_manager,1,1,1,1
access_git_config_root,Git Config Root,model_cx_tower_git_project,cetmix_tower_server.group_root,1,1,1,1
access_git_source_manager,Git Source Manager,model_cx_tower_git_source,cetmix_tower_server.group_manager,1,1,1,1
access_git_source_root,Git Source Root,model_cx_tower_git_source,cetmix_tower_server.group_root,1,1,1,1
access_git_remote_manager,Git Remote Manager,model_cx_tower_git_remote,cetmix_tower_server.group_manager,1,1,1,1
access_git_remote_root,Git Remote Root,model_cx_tower_git_remote,cetmix_tower_server.group_root,1,1,1,1
access_git_repo_manager,Git Repository Manager,model_cx_tower_git_repo,cetmix_tower_server.group_manager,1,1,1,1
access_git_repo_root,Git Repository Root,model_cx_tower_git_repo,cetmix_tower_server.group_root,1,1,1,1
access_git_repo_owner_manager,Git Repository Owner Manager,model_cx_tower_git_repo_owner,cetmix_tower_server.group_manager,1,1,1,0
access_git_repo_owner_root,Git Repository Owner Root,model_cx_tower_git_repo_owner,cetmix_tower_server.group_root,1,1,1,1
access_git_project_server_file_rel,Git Project Server File Rel Manager,model_cx_tower_git_project_rel,cetmix_tower_server.group_manager,1,1,1,1
access_git_project_server_file_rel_root,Git Project Server File Rel Root,model_cx_tower_git_project_rel,cetmix_tower_server.group_root,1,1,1,1
access_git_project_file_template_rel,Git Project File Template Rel Manager,model_cx_tower_git_project_file_template_rel,cetmix_tower_server.group_manager,1,1,1,1
access_git_project_file_template_rel_root,Git Project File Template Rel Root,model_cx_tower_git_project_file_template_rel,cetmix_tower_server.group_root,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_git_config_manager Git Config Manager model_cx_tower_git_project cetmix_tower_server.group_manager 1 1 1 1
3 access_git_config_root Git Config Root model_cx_tower_git_project cetmix_tower_server.group_root 1 1 1 1
4 access_git_source_manager Git Source Manager model_cx_tower_git_source cetmix_tower_server.group_manager 1 1 1 1
5 access_git_source_root Git Source Root model_cx_tower_git_source cetmix_tower_server.group_root 1 1 1 1
6 access_git_remote_manager Git Remote Manager model_cx_tower_git_remote cetmix_tower_server.group_manager 1 1 1 1
7 access_git_remote_root Git Remote Root model_cx_tower_git_remote cetmix_tower_server.group_root 1 1 1 1
8 access_git_repo_manager Git Repository Manager model_cx_tower_git_repo cetmix_tower_server.group_manager 1 1 1 1
9 access_git_repo_root Git Repository Root model_cx_tower_git_repo cetmix_tower_server.group_root 1 1 1 1
10 access_git_repo_owner_manager Git Repository Owner Manager model_cx_tower_git_repo_owner cetmix_tower_server.group_manager 1 1 1 0
11 access_git_repo_owner_root Git Repository Owner Root model_cx_tower_git_repo_owner cetmix_tower_server.group_root 1 1 1 1
12 access_git_project_server_file_rel Git Project Server File Rel Manager model_cx_tower_git_project_rel cetmix_tower_server.group_manager 1 1 1 1
13 access_git_project_server_file_rel_root Git Project Server File Rel Root model_cx_tower_git_project_rel cetmix_tower_server.group_root 1 1 1 1
14 access_git_project_file_template_rel Git Project File Template Rel Manager model_cx_tower_git_project_file_template_rel cetmix_tower_server.group_manager 1 1 1 1
15 access_git_project_file_template_rel_root Git Project File Template Rel Root model_cx_tower_git_project_file_template_rel cetmix_tower_server.group_root 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,450 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Cetmix Tower Git</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="cetmix-tower-git">
<h1 class="title">Cetmix Tower Git</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:2a3dd557b4ae104188b8e2912c02b135bcfe7cc6518f8633daa149ccb94850fe
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_git"><img alt="cetmix/cetmix-tower" src="https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github" /></a></p>
<p>This module implements Git Management functionality for <a class="reference external" href="https://cetmix.com/tower">Cetmix
Tower</a>.</p>
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
documentation</a> for detailed information.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
<li><a class="reference internal" href="#changelog" id="toc-entry-3">Changelog</a><ul>
<li><a class="reference internal" href="#section-1" id="toc-entry-4">18.0.1.0.2 (2026-03-10)</a></li>
<li><a class="reference internal" href="#section-2" id="toc-entry-5">18.0.1.0.1 (2025-12-17)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-6">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-7">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-8">Authors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-9">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
documentation</a> for detailed configuration
instructions.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
documentation</a> for detailed usage
instructions.</p>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-3">Changelog</a></h1>
<div class="section" id="section-1">
<h2><a class="toc-backref" href="#toc-entry-4">18.0.1.0.2 (2026-03-10)</a></h2>
<ul class="simple">
<li>Features: Provide git project name using the <tt class="docutils literal">__git_project__</tt>
custom value when creating a project in flight plan. Improve the UI
and UX of Git Projects. (5197)</li>
<li>Bugfixes: Link server to git project only once. (5214)</li>
</ul>
</div>
<div class="section" id="section-2">
<h2><a class="toc-backref" href="#toc-entry-5">18.0.1.0.1 (2025-12-17)</a></h2>
<ul class="simple">
<li>Features: Improve search views, implement the search panel for
selected views. (5139)</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-6">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_git%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-7">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-8">Authors</a></h2>
<ul class="simple">
<li>Cetmix</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-9">Maintainers</a></h2>
<p>This module is part of the <a class="reference external" href="https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_git">cetmix/cetmix-tower</a> project on GitHub.</p>
<p>You are welcome to contribute.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,7 @@
from . import test_remote
from . import test_source
from . import test_project
from . import test_file_rel
from . import test_file_template_rel
from . import test_server
from . import test_repo

View File

@@ -0,0 +1,136 @@
from odoo.addons.cetmix_tower_server.tests.common import TestTowerCommon
class CommonTest(TestTowerCommon):
"""Common test class for all tests."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Models
cls.GitProject = cls.env["cx.tower.git.project"]
cls.GitProjectRel = cls.env["cx.tower.git.project.rel"]
cls.GitProjectFileTemplateRel = cls.env[
"cx.tower.git.project.file.template.rel"
]
cls.GitSource = cls.env["cx.tower.git.source"]
cls.GitRemote = cls.env["cx.tower.git.remote"]
# Data
# Project
cls.git_project_1 = cls.GitProject.create({"name": "Git Project 1"})
# Sources
cls.git_source_1 = cls.GitSource.create(
{"name": "Git Source 1", "git_project_id": cls.git_project_1.id}
)
cls.git_source_2 = cls.GitSource.create(
{"name": "Git Source 2", "git_project_id": cls.git_project_1.id}
)
# Repositories
cls.Repo = cls.env["cx.tower.git.repo"]
cls.RepoOwner = cls.env["cx.tower.git.repo.owner"]
cls.repo_cetmix_tower = cls.Repo.create(
{
"name": "Cetmix Tower",
"url": "https://github.com/cetmix-test/cetmix-tower-test.git",
}
)
cls.repo_oca_web = cls.Repo.create(
{
"name": "OCA Web",
"url": "https://github.com/oca-test/web-test.git",
}
)
cls.repo_odoo_enterprise = cls.Repo.create(
{
"name": "Odoo Enterprise",
"url": "https://github.com/odoo-test/enterprise-test.git",
"is_private": True,
}
)
cls.repo_gitlab_private = cls.Repo.create(
{
"name": "GitLab Private",
"url": "git@my.gitlab.com:cetmix-test/cetmix-tower-test.git",
"is_private": True,
}
)
cls.repo_bitbucket_private = cls.Repo.create(
{
"name": "Bitbucket Private",
"url": "https://bitbucket.com/cetmix-test/cetmix-tower-test-enterprise.git",
"is_private": True,
}
)
# Same urls, different protocols (intentionally aliased)
cls.repo_other_ssh = cls.Repo.create(
{"url": "git@memegit.com:cetmix-test/cetmix-tower-test.git"}
)
cls.repo_other_https = cls.repo_other_ssh
# Remotes
cls.remote_github_https = cls.GitRemote.create(
{
"repo_id": cls.repo_cetmix_tower.id,
"source_id": cls.git_source_1.id,
"head_type": "pr",
"head": "https://github.com/cetmix-test/cetmix-tower-test/pull/123",
"sequence": 1,
}
)
cls.remote_gitlab_https = cls.GitRemote.create(
{
"repo_id": cls.repo_gitlab_private.id,
"source_id": cls.git_source_1.id,
"head_type": "branch",
"head": "main",
"sequence": 2,
}
)
cls.remote_gitlab_ssh = cls.GitRemote.create(
{
"repo_id": cls.repo_gitlab_private.id,
"source_id": cls.git_source_1.id,
"head_type": "commit",
"url_protocol": "ssh",
"head": "10000000",
"sequence": 3,
}
)
cls.remote_bitbucket_https = cls.GitRemote.create(
{
"repo_id": cls.repo_bitbucket_private.id,
"source_id": cls.git_source_2.id,
"head_type": "branch",
"head": "dev",
"sequence": 4,
}
)
cls.remote_other_ssh = cls.GitRemote.create(
{
"repo_id": cls.repo_other_ssh.id,
"source_id": cls.git_source_2.id,
"head_type": "branch",
"url_protocol": "ssh",
"head": "old",
"sequence": 5,
}
)
# File
cls.server_1_file_1 = cls.File.create(
{
"name": "File 1",
"server_id": cls.server_test_1.id,
"source": "tower",
}
)
cls.file_template_1 = cls.FileTemplate.create(
{
"name": "File Template 1",
}
)

View File

@@ -0,0 +1,390 @@
from odoo.exceptions import AccessError
from .common import CommonTest
class TestFileRel(CommonTest):
"""Test class for git file relation."""
def setUp(self):
super().setUp()
self.file_1_rel = self.GitProjectRel.create(
{
"server_id": self.server_test_1.id,
"file_id": self.server_1_file_1.id,
"git_project_id": self.git_project_1.id,
"project_format": "git_aggregator",
}
)
def test_file_rel_create(self):
"""Test if file relation is created correctly"""
# -- 1 --
# Check if file content is updated
# Get code from project
yaml_code_from_project = (
self.file_1_rel.git_project_id._generate_code_git_aggregator(
self.file_1_rel
)
)
self.assertEqual(
self.server_1_file_1.code,
yaml_code_from_project,
"File content is not updated correctly",
)
# Check specific if remote is present in file
self.assertIn(
self.remote_other_ssh.repo_id.url_ssh,
self.server_1_file_1.code,
"Remote is not present in file",
)
# -- 2 --
# Modify remove and check if file content is updated
self.remote_other_ssh.repo_id = self.Repo.create(
{
"url": "https://github.com/cetmix/cetmix-memes.git",
}
)
self.remote_other_ssh.url_protocol = "https"
# Must be different from previous project code
self.assertNotEqual(
self.server_1_file_1.code,
yaml_code_from_project,
"File content is not updated correctly",
)
# New remote must be present in file
self.assertIn(
"https://github.com/cetmix/cetmix-memes.git",
self.server_1_file_1.code,
"Remote is not present in file",
)
# -- 3 --
# Disable source and check if file content is updated
self.git_source_2.active = False
self.assertNotIn(
"https://github.com/cetmix/cetmix-memes.git",
self.server_1_file_1.code,
"Remote is present in file",
)
def test_format_git_aggregator(self):
"""Test if format git aggregator works correctly"""
# -- 1 --
# Check if YAML code is generated correctly
yaml_code = """# This file is generated with Cetmix Tower https://cetmix.com/tower
# It's designed to be used with git-aggregator tool developed by Acsone.
# Documentation for git-aggregator: https://github.com/acsone/git-aggregator
# You need to set the following variables in your environment:
# BITBUCKET_TOKEN, GITLAB_TOKEN, GITLAB_TOKEN_NAME
# and run git-aggregator with '--expand-env' parameter.
./git_project_1_git_source_1:
remotes:
remote_1: https://github.com/cetmix-test/cetmix-tower-test.git
remote_2: https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@my.gitlab.com/cetmix-test/cetmix-tower-test.git
remote_3: git@my.gitlab.com:cetmix-test/cetmix-tower-test.git
merges:
- remote: remote_1
ref: refs/pull/123/head
- remote: remote_2
ref: main
- remote: remote_3
ref: '10000000'
target: remote_1
./git_project_1_git_source_1_2:
remotes:
remote_1: https://x-token-auth:$BITBUCKET_TOKEN@bitbucket.com/cetmix-test/cetmix-tower-test-enterprise.git
remote_2: git@memegit.com:cetmix-test/cetmix-tower-test.git
merges:
- remote: remote_1
ref: dev
- remote: remote_2
ref: old
target: remote_1
""" # noqa: E501
# Get code from project
yaml_code_from_project = (
self.file_1_rel.git_project_id._generate_code_git_aggregator(
self.file_1_rel
)
)
self.assertEqual(
yaml_code_from_project,
yaml_code,
"YAML code is not generated correctly",
)
# -- 2 --
# Unlink remote and check if file content is updated
self.remote_github_https.unlink()
yaml_code_from_project = (
self.file_1_rel.git_project_id._generate_code_git_aggregator(
self.file_1_rel
)
)
yaml_code = """# This file is generated with Cetmix Tower https://cetmix.com/tower
# It's designed to be used with git-aggregator tool developed by Acsone.
# Documentation for git-aggregator: https://github.com/acsone/git-aggregator
# You need to set the following variables in your environment:
# BITBUCKET_TOKEN, GITLAB_TOKEN, GITLAB_TOKEN_NAME
# and run git-aggregator with '--expand-env' parameter.
./git_project_1_git_source_1:
remotes:
remote_2: https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@my.gitlab.com/cetmix-test/cetmix-tower-test.git
remote_3: git@my.gitlab.com:cetmix-test/cetmix-tower-test.git
merges:
- remote: remote_2
ref: main
- remote: remote_3
ref: '10000000'
target: remote_2
./git_project_1_git_source_1_2:
remotes:
remote_1: https://x-token-auth:$BITBUCKET_TOKEN@bitbucket.com/cetmix-test/cetmix-tower-test-enterprise.git
remote_2: git@memegit.com:cetmix-test/cetmix-tower-test.git
merges:
- remote: remote_1
ref: dev
- remote: remote_2
ref: old
target: remote_1
""" # noqa: E501
self.assertEqual(
yaml_code_from_project,
yaml_code,
"YAML code is not generated correctly",
)
# -- 3 --
# Unlink source and check if file content is updated
self.git_source_2.unlink()
yaml_code_from_project = (
self.file_1_rel.git_project_id._generate_code_git_aggregator(
self.file_1_rel
)
)
yaml_code = """# This file is generated with Cetmix Tower https://cetmix.com/tower
# It's designed to be used with git-aggregator tool developed by Acsone.
# Documentation for git-aggregator: https://github.com/acsone/git-aggregator
# You need to set the following variables in your environment:
# GITLAB_TOKEN, GITLAB_TOKEN_NAME
# and run git-aggregator with '--expand-env' parameter.
./git_project_1_git_source_1:
remotes:
remote_2: https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@my.gitlab.com/cetmix-test/cetmix-tower-test.git
remote_3: git@my.gitlab.com:cetmix-test/cetmix-tower-test.git
merges:
- remote: remote_2
ref: main
- remote: remote_3
ref: '10000000'
target: remote_2
""" # noqa: E501
self.assertEqual(
yaml_code_from_project,
yaml_code,
"YAML code is not generated correctly",
)
def test_user_access(self):
"""Test that regular users have no access to git project relations"""
user_rel = self.GitProjectRel.with_user(self.user)
# Try create - should fail
with self.assertRaises(AccessError):
user_rel.create(
{
"server_id": self.server_test_1.id,
"file_id": self.server_1_file_1.id,
"git_project_id": self.git_project_1.id,
"project_format": "git_aggregator",
}
)
# Try read - should fail
with self.assertRaises(AccessError):
user_rel.browse(self.file_1_rel.id).read(["name"])
# Try write - should fail
with self.assertRaises(AccessError):
user_rel.browse(self.file_1_rel.id).write(
{"project_format": "git_aggregator"}
)
# Try unlink - should fail
with self.assertRaises(AccessError):
user_rel.browse(self.file_1_rel.id).unlink()
def test_manager_read_access(self):
"""Test manager read access rules"""
manager_rel = self.GitProjectRel.with_user(self.manager)
# Initially manager should not have access
with self.assertRaises(AccessError):
manager_rel.browse(self.file_1_rel.id).read(["name"])
# Add manager as project user - should have read access
self.git_project_1.write({"user_ids": [(4, self.manager.id)]})
self.assertEqual(manager_rel.browse(self.file_1_rel.id).name, "Git Project 1")
# Remove from project, add as server user - should have read access
self.git_project_1.write({"user_ids": [(3, self.manager.id)]})
self.server_test_1.write({"user_ids": [(4, self.manager.id)]})
self.assertEqual(manager_rel.browse(self.file_1_rel.id).name, "Git Project 1")
# Remove from server users, add as project manager - should have read access
self.server_test_1.write({"user_ids": [(3, self.manager.id)]})
self.git_project_1.write({"manager_ids": [(4, self.manager.id)]})
self.assertEqual(manager_rel.browse(self.file_1_rel.id).name, "Git Project 1")
# Remove from project, add as server manager - should have read access
self.git_project_1.write({"manager_ids": [(3, self.manager.id)]})
self.server_test_1.write({"manager_ids": [(4, self.manager.id)]})
self.assertEqual(manager_rel.browse(self.file_1_rel.id).name, "Git Project 1")
def test_manager_write_access(self):
"""Test manager write/create access rules"""
manager_rel = self.GitProjectRel.with_user(self.manager)
# Create new file to avoid unique constraint violation
file_2 = self.File.create(
{
"name": "test_file_2",
"server_id": self.server_test_1.id,
"source": "tower",
"file_type": "text",
}
)
# Try create without being project and server manager - should fail
with self.assertRaises(AccessError):
manager_rel.create(
{
"server_id": self.server_test_1.id,
"file_id": file_2.id,
"git_project_id": self.git_project_1.id,
"project_format": "git_aggregator",
}
)
# Add as project manager only - should still fail
file_3 = self.File.create(
{
"name": "test_file_3",
"server_id": self.server_test_1.id,
"source": "tower",
"file_type": "text",
}
)
self.git_project_1.write({"manager_ids": [(4, self.manager.id)]})
with self.assertRaises(AccessError):
manager_rel.create(
{
"server_id": self.server_test_1.id,
"file_id": file_3.id,
"git_project_id": self.git_project_1.id,
"project_format": "git_aggregator",
}
)
# Add as server manager - should succeed
file_4 = self.File.create(
{
"name": "test_file_4",
"server_id": self.server_test_1.id,
"source": "tower",
"file_type": "text",
}
)
self.server_test_1.write({"manager_ids": [(4, self.manager.id)]})
rel = manager_rel.create(
{
"server_id": self.server_test_1.id,
"file_id": file_4.id,
"git_project_id": self.git_project_1.id,
"project_format": "git_aggregator",
}
)
self.assertTrue(rel.exists())
# Test write access
rel.write({"project_format": "git_aggregator"})
# Remove server manager access - should fail to write
self.server_test_1.write({"manager_ids": [(3, self.manager.id)]})
with self.assertRaises(AccessError):
rel.write({"project_format": "git_aggregator"})
# Remove project manager access - should fail to write
self.git_project_1.write({"manager_ids": [(3, self.manager.id)]})
with self.assertRaises(AccessError):
rel.write({"project_format": "git_aggregator"})
def test_manager_unlink_access(self):
"""Test manager unlink access rules"""
manager_rel = self.GitProjectRel.with_user(self.manager)
# Try delete without being project and server manager - should fail
with self.assertRaises(AccessError):
manager_rel.browse(self.file_1_rel.id).unlink()
# Add as project manager only - should fail
self.git_project_1.write({"manager_ids": [(4, self.manager.id)]})
with self.assertRaises(AccessError):
manager_rel.browse(self.file_1_rel.id).unlink()
# Add as server manager - should succeed
self.server_test_1.write({"manager_ids": [(4, self.manager.id)]})
self.file_1_rel.with_user(self.manager).unlink()
self.assertFalse(self.file_1_rel.exists())
def test_root_access(self):
"""Test root access rules"""
root_rel = self.GitProjectRel.with_user(self.root)
# Create new file to avoid unique constraint violation
file_3 = self.File.create(
{
"name": "test_file_3",
"server_id": self.server_test_1.id,
"source": "tower",
"file_type": "text",
}
)
# Create - should succeed
rel = root_rel.create(
{
"server_id": self.server_test_1.id,
"file_id": file_3.id,
"git_project_id": self.git_project_1.id,
"project_format": "git_aggregator",
}
)
self.assertTrue(rel.exists())
# Read - should succeed
self.assertEqual(root_rel.browse(rel.id).name, "Git Project 1")
# Write - should succeed
root_rel.browse(rel.id).write({"project_format": "git_aggregator"})
# Delete - should succeed
rel.unlink()
self.assertFalse(rel.exists())

View File

@@ -0,0 +1,308 @@
from odoo.exceptions import AccessError
from .common import CommonTest
class TestFileTemplateRel(CommonTest):
"""Test class for git file template relation."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.file_template_1_rel = cls.GitProjectFileTemplateRel.create(
{
"git_project_id": cls.git_project_1.id,
"file_template_id": cls.file_template_1.id,
"project_format": "git_aggregator",
}
)
def test_file_template_rel_create(self):
"""Test if file template relation is created correctly"""
# -- 1 --
# Check if file content is updated
# Get code from project
yaml_code_from_project = (
self.file_template_1_rel.git_project_id._generate_code_git_aggregator(
self.file_template_1_rel
)
)
self.assertEqual(
self.file_template_1.code,
yaml_code_from_project,
"File template content is not updated correctly",
)
# Check specific if remote is present in file
self.assertIn(
self.remote_other_ssh.repo_id.url_ssh,
self.file_template_1.code,
"Remote is not present in file template",
)
# -- 2 --
# Modify remove and check if file template content is updated
self.remote_other_ssh.repo_id = self.Repo.create(
{
"url": "https://github.com/cetmix/cetmix-memes.git",
}
)
self.remote_other_ssh.url_protocol = "https"
# Must be different from previous project code
self.assertNotEqual(
self.file_template_1.code,
yaml_code_from_project,
"File template content is not updated correctly",
)
# New remote must be present in file
self.assertIn(
"https://github.com/cetmix/cetmix-memes.git",
self.file_template_1.code,
"Remote is not present in file template",
)
# -- 3 --
# Disable source and check if file content is updated
self.git_source_2.active = False
self.assertNotIn(
"https://github.com/cetmix/cetmix-memes.git",
self.file_template_1.code,
"Remote is present in file template",
)
def test_format_git_aggregator(self):
"""Test if format git aggregator works correctly"""
# -- 1 --
# Check if YAML code is generated correctly
yaml_code = """# This file is generated with Cetmix Tower https://cetmix.com/tower
# It's designed to be used with git-aggregator tool developed by Acsone.
# Documentation for git-aggregator: https://github.com/acsone/git-aggregator
# You need to set the following variables in your environment:
# BITBUCKET_TOKEN, GITLAB_TOKEN, GITLAB_TOKEN_NAME
# and run git-aggregator with '--expand-env' parameter.
./git_project_1_git_source_1:
remotes:
remote_1: https://github.com/cetmix-test/cetmix-tower-test.git
remote_2: https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@my.gitlab.com/cetmix-test/cetmix-tower-test.git
remote_3: git@my.gitlab.com:cetmix-test/cetmix-tower-test.git
merges:
- remote: remote_1
ref: refs/pull/123/head
- remote: remote_2
ref: main
- remote: remote_3
ref: '10000000'
target: remote_1
./git_project_1_git_source_1_2:
remotes:
remote_1: https://x-token-auth:$BITBUCKET_TOKEN@bitbucket.com/cetmix-test/cetmix-tower-test-enterprise.git
remote_2: git@memegit.com:cetmix-test/cetmix-tower-test.git
merges:
- remote: remote_1
ref: dev
- remote: remote_2
ref: old
target: remote_1
""" # noqa: E501
# Get code from project
yaml_code_from_project = (
self.file_template_1_rel.git_project_id._generate_code_git_aggregator(
self.file_template_1_rel
)
)
self.assertEqual(
yaml_code_from_project,
yaml_code,
"YAML code is not generated correctly",
)
def test_user_access(self):
"""Test that regular users have no access to git project relations"""
user_rel = self.GitProjectFileTemplateRel.with_user(self.user)
# Try create - should fail
with self.assertRaises(AccessError):
user_rel.create(
{
"git_project_id": self.git_project_1.id,
"file_template_id": self.file_template_1.id,
"project_format": "git_aggregator",
}
)
# Try read - should fail
with self.assertRaises(AccessError):
user_rel.browse(self.file_template_1_rel.id).read(["name"])
# Try write - should fail
with self.assertRaises(AccessError):
user_rel.browse(self.file_template_1_rel.id).write(
{"project_format": "git_aggregator"}
)
# Try unlink - should fail
with self.assertRaises(AccessError):
user_rel.browse(self.file_template_1_rel.id).unlink()
def test_manager_read_access(self):
"""Test manager read access rules"""
manager_rel = self.GitProjectFileTemplateRel.with_user(self.manager)
# Initially manager should not have access
with self.assertRaises(AccessError):
manager_rel.browse(self.file_template_1_rel.id).read(["name"])
# Add manager as project user - should have read access
self.git_project_1.write({"user_ids": [(4, self.manager.id)]})
self.assertEqual(
manager_rel.browse(self.file_template_1_rel.id).name, "Git Project 1"
)
# Remove from project, add as file template user
# should have read access
self.git_project_1.write({"user_ids": [(3, self.manager.id)]})
self.file_template_1.write({"user_ids": [(4, self.manager.id)]})
self.assertEqual(
manager_rel.browse(self.file_template_1_rel.id).name, "Git Project 1"
)
# Remove from file template users, add as project manager
# should have read access
self.file_template_1.write({"user_ids": [(3, self.manager.id)]})
self.git_project_1.write({"manager_ids": [(4, self.manager.id)]})
self.assertEqual(
manager_rel.browse(self.file_template_1_rel.id).name, "Git Project 1"
)
# Remove from project, add as file template manager
# should have read access
self.git_project_1.write({"manager_ids": [(3, self.manager.id)]})
self.file_template_1.write({"manager_ids": [(4, self.manager.id)]})
self.assertEqual(
manager_rel.browse(self.file_template_1_rel.id).name, "Git Project 1"
)
def test_manager_write_access(self):
"""Test manager write/create access rules"""
manager_rel = self.GitProjectFileTemplateRel.with_user(self.manager)
# Create new file template to avoid unique constraint violation
file_template_2 = self.FileTemplate.create(
{
"name": "test_file_template_2",
}
)
# Try create without being project and file template manager - should fail
with self.assertRaises(AccessError):
manager_rel.create(
{
"git_project_id": self.git_project_1.id,
"file_template_id": file_template_2.id,
"project_format": "git_aggregator",
}
)
# Add as project manager only - should still fail
file_template_3 = self.FileTemplate.create(
{
"name": "test_file_template_3",
}
)
self.git_project_1.write({"manager_ids": [(4, self.manager.id)]})
with self.assertRaises(AccessError):
manager_rel.create(
{
"git_project_id": self.git_project_1.id,
"file_template_id": file_template_3.id,
"project_format": "git_aggregator",
}
)
# Add as file template manager - should succeed
file_template_4 = self.FileTemplate.create(
{
"name": "test_file_template_4",
}
)
file_template_4.write({"manager_ids": [(4, self.manager.id)]})
rel = manager_rel.create(
{
"git_project_id": self.git_project_1.id,
"file_template_id": file_template_4.id,
"project_format": "git_aggregator",
}
)
self.assertTrue(rel.exists())
# Test write access
rel.write({"project_format": "git_aggregator"})
# Remove file template manager access - should fail to write
file_template_4.write({"manager_ids": [(3, self.manager.id)]})
with self.assertRaises(AccessError):
rel.write({"project_format": "git_aggregator"})
# Remove project manager access - should fail to write
self.git_project_1.write({"manager_ids": [(3, self.manager.id)]})
file_template_4.write({"manager_ids": [(4, self.manager.id)]})
with self.assertRaises(AccessError):
rel.write({"project_format": "git_aggregator"})
def test_manager_unlink_access(self):
"""Test manager unlink access rules"""
manager_rel = self.GitProjectFileTemplateRel.with_user(self.manager)
# Try delete without being project and server manager - should fail
with self.assertRaises(AccessError):
manager_rel.browse(self.file_template_1_rel.id).unlink()
# Add as project manager only - should fail
self.git_project_1.write({"manager_ids": [(4, self.manager.id)]})
with self.assertRaises(AccessError):
manager_rel.browse(self.file_template_1_rel.id).unlink()
# Add as file template manager - should succeed
self.file_template_1.write({"manager_ids": [(4, self.manager.id)]})
self.file_template_1_rel.unlink()
self.assertFalse(self.file_template_1_rel.exists())
def test_root_access(self):
"""Test root access rules"""
root_rel = self.GitProjectFileTemplateRel.with_user(self.root)
# Create new file to avoid unique constraint violation
file_template_3 = self.FileTemplate.create(
{
"name": "test_file_template_3",
}
)
# Create - should succeed
rel = root_rel.create(
{
"git_project_id": self.git_project_1.id,
"file_template_id": file_template_3.id,
"project_format": "git_aggregator",
}
)
self.assertTrue(rel.exists())
# Read - should succeed
self.assertEqual(root_rel.browse(rel.id).name, "Git Project 1")
# Write - should succeed
root_rel.browse(rel.id).write({"project_format": "git_aggregator"})
# Delete - should succeed
rel.unlink()
self.assertFalse(rel.exists())

View File

@@ -0,0 +1,315 @@
from odoo.exceptions import AccessError
from .common import CommonTest
class TestProject(CommonTest):
"""Test class for git project."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Remove user bob from all groups
cls.remove_from_group(
cls.user_bob,
[
"cetmix_tower_server.group_user",
"cetmix_tower_server.group_manager",
"cetmix_tower_server.group_root",
],
)
# Create another manager for testing
cls.manager_2 = cls.Users.create(
{
"name": "Second Manager",
"login": "manager2",
"email": "manager2@test.com",
"groups_id": [(4, cls.env.ref("cetmix_tower_server.group_manager").id)],
}
)
# Create test project as root
cls.project = cls.GitProject.create(
{
"name": "Test Project",
}
)
def test_user_access(self):
"""Test that regular users have no access to git projects"""
user_project = self.GitProject.with_user(self.user)
# Test CRUD operations
with self.assertRaises(AccessError):
user_project.create({"name": "New Project"})
with self.assertRaises(AccessError):
user_project.browse(self.project.id).read(["name"])
with self.assertRaises(AccessError):
user_project.browse(self.project.id).write({"name": "Updated Name"})
with self.assertRaises(AccessError):
user_project.browse(self.project.id).unlink()
def test_manager_read_access(self):
"""Test manager read access rules"""
manager_project = self.GitProject.with_user(self.manager)
# Manager not in user_ids or manager_ids - should not read
with self.assertRaises(AccessError):
manager_project.browse(self.project.id).read(["name"])
# Add manager to user_ids - should read
self.project.write({"user_ids": [(4, self.manager.id)]})
self.assertEqual(manager_project.browse(self.project.id).name, "Test Project")
# Remove from user_ids, add to manager_ids - should read
self.project.write(
{"user_ids": [(3, self.manager.id)], "manager_ids": [(4, self.manager.id)]}
)
self.assertEqual(manager_project.browse(self.project.id).name, "Test Project")
def test_manager_write_access(self):
"""Test manager write/create access rules"""
manager_project = self.GitProject.with_user(self.manager)
# Create - should succeed as manager is added by default
new_project = manager_project.create({"name": "New Project"})
self.assertTrue(new_project.exists())
self.assertIn(self.manager, new_project.manager_ids)
# Write - not in manager_ids, should fail
with self.assertRaises(AccessError):
manager_project.browse(self.project.id).write({"name": "Updated Name"})
# Add to manager_ids - should write
self.project.write({"manager_ids": [(4, self.manager.id)]})
manager_project.browse(self.project.id).write({"name": "Updated Name"})
self.assertEqual(self.project.name, "Updated Name")
def test_manager_unlink_access(self):
"""Test manager unlink access rules"""
# Create project as manager_2
project = self.GitProject.with_user(self.manager_2).create(
{"name": "Project to Delete"}
)
manager_project = self.GitProject.with_user(self.manager)
# Try delete as different manager - should fail
with self.assertRaises(AccessError):
manager_project.browse(project.id).unlink()
# Add to manager_ids but not creator - should fail
project.write({"manager_ids": [(4, self.manager.id)]})
with self.assertRaises(AccessError):
manager_project.browse(project.id).unlink()
# Create as manager and try delete - should succeed
own_project = manager_project.create({"name": "Own Project"})
self.assertTrue(own_project.exists())
own_project.unlink()
self.assertFalse(own_project.exists())
def test_root_access(self):
"""Test root access rules"""
root_project = self.GitProject.with_user(self.root)
# Create
new_project = root_project.create({"name": "Root Project"})
self.assertTrue(new_project.exists())
# Read
self.assertEqual(root_project.browse(self.project.id).name, "Test Project")
# Write
root_project.browse(self.project.id).write({"name": "Updated by Root"})
self.assertEqual(self.project.name, "Updated by Root")
# Delete
new_project.unlink()
self.assertFalse(new_project.exists())
def test_compute_user_ids(self):
"""Test computation of user_ids and manager_ids for git projects"""
# Add users "Bob" and "user" to the group "cetmix_tower_server.group_manager"
self.add_to_group(self.user_bob, "cetmix_tower_server.group_manager")
self.add_to_group(self.user, "cetmix_tower_server.group_manager")
# -- 1 --
# Create project as manager
project_as_manager = self.GitProject.with_user(self.manager).create(
{
"name": "Project As Manager",
}
)
# Check that manager is added to both user_ids and manager_ids by default
self.assertEqual(len(project_as_manager.user_ids), 1)
self.assertIn(self.manager, project_as_manager.user_ids)
self.assertEqual(len(project_as_manager.manager_ids), 1)
self.assertIn(self.manager, project_as_manager.manager_ids)
# -- 2 --
# Create servers with multiple users and managers
server_1 = self.Server.create(
{
"name": "Test Server 1",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"os_id": self.os_debian_10.id,
"user_ids": [(6, 0, [self.user_bob.id, self.user.id])], # Two users
"manager_ids": [
(6, 0, [self.manager.id, self.manager_2.id])
], # Two managers
}
)
server_2 = self.Server.create(
{
"name": "Test Server 2",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"os_id": self.os_debian_10.id,
"user_ids": [
(6, 0, [self.user_bob.id, self.user.id])
], # Same two users
"manager_ids": [
(6, 0, [self.manager.id, self.manager_2.id])
], # Same two managers
}
)
# Create project and link servers
project = self.GitProject.create(
{
"name": "Test Project",
}
)
# Create files and link them to the project
for server in [server_1, server_2]:
file = self.File.create(
{
"name": f"test_file_{server.name}",
"server_id": server.id,
}
)
self.GitProjectRel.create(
{
"server_id": server.id,
"file_id": file.id,
"git_project_id": project.id,
"project_format": "git_aggregator",
}
)
# Invalidate cache to ensure computed fields are updated
project.invalidate_recordset(["server_ids", "user_ids", "manager_ids"])
# -- 3 --
# Test computed values with linked servers
# Each user/manager should be counted only once even if present in both servers
self.assertEqual(len(project.server_ids), 2)
self.assertEqual(len(project.user_ids), 2) # Two unique users
self.assertIn(self.user_bob, project.user_ids)
self.assertIn(self.user, project.user_ids)
self.assertEqual(len(project.manager_ids), 2) # Two unique managers
self.assertIn(self.manager, project.manager_ids)
self.assertIn(self.manager_2, project.manager_ids)
# -- 4 --
# Add server with different users/managers
server_3 = self.Server.create(
{
"name": "Test Server 3",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"os_id": self.os_debian_10.id,
"user_ids": [(6, 0, [self.user_bob.id])], # Only one user
"manager_ids": [(6, 0, [self.manager_2.id])], # Only second manager
}
)
file_3 = self.File.create(
{
"name": "test_file_3",
"server_id": server_3.id,
}
)
self.GitProjectRel.create(
{
"server_id": server_3.id,
"file_id": file_3.id,
"git_project_id": project.id,
"project_format": "git_aggregator",
}
)
# Invalidate cache to ensure computed fields are updated
project.invalidate_recordset(["server_ids", "user_ids", "manager_ids"])
# Test that computed values are updated correctly
# Only users/managers present in all servers should remain
self.assertEqual(len(project.server_ids), 3)
self.assertEqual(len(project.user_ids), 1) # Only bob is in all servers
self.assertIn(self.user_bob, project.user_ids)
self.assertEqual(
len(project.manager_ids), 1
) # Only manager_2 is in all servers
self.assertIn(self.manager_2, project.manager_ids)
# -- 5 --
# Verify that first manager can still access the project
project_as_manager_1 = self.GitProject.with_user(self.manager).browse(
project.id
)
self.assertTrue(project_as_manager_1.exists())
self.assertEqual(project_as_manager_1.name, "Test Project")
def test_manager_server_based_access(self):
"""Test manager access through server relationships"""
manager_project = self.GitProject.with_user(self.manager)
# Create a server where manager is a user
server = self.Server.create(
{
"name": "Test Server",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"os_id": self.os_debian_10.id,
"user_ids": [(4, self.manager.id)],
}
)
# Create a file and link project to server
file = self.File.create(
{
"name": "test_file",
"server_id": server.id,
}
)
self.GitProjectRel.create(
{
"server_id": server.id,
"file_id": file.id,
"git_project_id": self.project.id,
"project_format": "git_aggregator",
}
)
# Manager should be able to read project through server relationship
self.assertEqual(manager_project.browse(self.project.id).name, "Test Project")
# Remove manager from server users
server.write({"user_ids": [(3, self.manager.id)]})
# Manager should not be able to read project anymore
with self.assertRaises(AccessError):
manager_project.browse(self.project.id).read(["name"])
# Add manager to server managers
server.write({"manager_ids": [(4, self.manager.id)]})
# Manager should be able to read project again
self.assertEqual(manager_project.browse(self.project.id).name, "Test Project")

View File

@@ -0,0 +1,462 @@
from odoo.exceptions import AccessError
from .common import CommonTest
class TestRemote(CommonTest):
"""Test class for git remote."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create another manager for testing
cls.manager_2 = cls.Users.create(
{
"name": "Second Manager",
"login": "manager2",
"email": "manager2@test.com",
"groups_id": [(4, cls.env.ref("cetmix_tower_server.group_manager").id)],
}
)
# Create test project and source as root
cls.project = cls.GitProject.create(
{
"name": "Test Project",
}
)
cls.source = cls.GitSource.create(
{
"name": "Test Source",
"git_project_id": cls.project.id,
}
)
cls.repo_cetmix_tower = cls.Repo.create(
{
"name": "Cetmix Tower",
"url": "https://github.com/cetmix-test/cetmix-tower.git",
}
)
cls.remote = cls.GitRemote.create(
{
"repo_id": cls.repo_cetmix_tower.id,
"source_id": cls.source.id,
"head_type": "branch",
"head": "main",
}
)
cls.repo_test = cls.Repo.create(
{
"name": "Test Repository",
"url": "https://github.com/cetmix-test/test.git",
}
)
def test_user_access(self):
"""Test that regular users have no access to git remotes"""
user_remote = self.GitRemote.with_user(self.user)
# Test CRUD operations
with self.assertRaises(AccessError):
user_remote.create(
{
"repo_id": self.repo_test.id,
"url_protocol": "https",
"source_id": self.source.id,
"head": "main",
}
)
with self.assertRaises(AccessError):
user_remote.search([("id", "=", self.remote.id)])
with self.assertRaises(AccessError):
self.remote.with_user(self.user).write({"head": "dev"})
with self.assertRaises(AccessError):
self.remote.with_user(self.user).unlink()
def test_manager_read_access(self):
"""Test manager read access rules"""
manager_remote = self.GitRemote.with_user(self.manager)
# Manager not in project user_ids or manager_ids - should not read
self.assertFalse(manager_remote.search([("id", "=", self.remote.id)]))
# Add manager to project user_ids - should read
self.project.write({"user_ids": [(4, self.manager.id)]})
remote = manager_remote.search([("id", "=", self.remote.id)])
self.assertTrue(remote)
self.assertEqual(remote.head, "main")
# Remove from user_ids, add to manager_ids - should read
self.project.write(
{"user_ids": [(3, self.manager.id)], "manager_ids": [(4, self.manager.id)]}
)
remote = manager_remote.search([("id", "=", self.remote.id)])
self.assertTrue(remote.exists())
def test_manager_write_access(self):
"""Test manager write/create access rules"""
manager_remote = self.GitRemote.with_user(self.manager)
# Create project as manager - should be added to manager_ids automatically
project = self.GitProject.with_user(self.manager).create(
{
"name": "Manager Project",
}
)
source = self.GitSource.with_user(self.manager).create(
{
"name": "Manager Source",
"git_project_id": project.id,
}
)
# Create remote in own project - should succeed
new_remote = manager_remote.create(
{
"repo_id": self.repo_test.id,
"url_protocol": "https",
"source_id": source.id,
"head_type": "branch",
"head": "main",
}
)
self.assertTrue(new_remote.exists())
# Write to own remote - should succeed
new_remote.write({"head": "dev"})
self.assertEqual(new_remote.head, "dev")
# Write to other's remote - should fail
with self.assertRaises(AccessError):
self.remote.with_user(self.manager).write({"head": "dev"})
def test_manager_unlink_access(self):
"""Test manager unlink access rules"""
# Create project and remote as manager_2
project = self.GitProject.with_user(self.manager_2).create(
{
"name": "Manager 2 Project",
}
)
source = self.GitSource.create(
{
"name": "Manager 2 Source",
"git_project_id": project.id,
}
)
remote = self.GitRemote.with_user(self.manager_2).create(
{
"repo_id": self.repo_test.id,
"url_protocol": "https",
"source_id": source.id,
"head_type": "branch",
"head": "main",
}
)
# Try delete as different manager - should fail even if added to manager_ids
project.write({"manager_ids": [(4, self.manager.id)]})
with self.assertRaises(AccessError):
remote.with_user(self.manager).unlink()
# Create remote as manager and try delete - should succeed
own_remote = self.GitRemote.with_user(self.manager).create(
{
"repo_id": self.repo_test.id,
"url_protocol": "https",
"source_id": source.id,
"head_type": "branch",
"head": "main",
}
)
self.assertTrue(own_remote.exists())
own_remote.with_user(self.manager).unlink()
self.assertFalse(own_remote.exists())
def test_root_access(self):
"""Test root access rules"""
root_remote = self.GitRemote.with_user(self.root)
# Create
new_remote = root_remote.create(
{
"repo_id": self.repo_test.id,
"url_protocol": "https",
"source_id": self.source.id,
"head_type": "branch",
"head": "main",
}
)
self.assertTrue(new_remote.exists())
# Read
remote = root_remote.search([("id", "=", self.remote.id)])
self.assertTrue(remote)
self.assertEqual(remote.head, "main")
# Write
self.remote.with_user(self.root).write({"head": "dev"})
self.assertEqual(self.remote.head, "dev")
# Delete
new_remote.with_user(self.root).unlink()
self.assertFalse(new_remote.exists())
def test_remote_provider_protocol_and_name(self):
"""Test if remote provider is detected correctly"""
# -- 1--
# GitHub + https
# Check if remote provider is detected correctly
self.assertEqual(
self.remote_github_https.repo_provider,
"github",
"Provider is not detected correctly",
)
self.assertEqual(
self.remote_github_https.url_protocol,
"https",
"Protocol is not detected correctly",
)
self.assertEqual(
self.remote_github_https.name,
"remote_1",
"Name is not prepared correctly",
)
# -- 2 --
# GitLab + ssh
# Check if remote provider is detected correctly
self.assertEqual(
self.remote_gitlab_ssh.repo_provider,
"gitlab",
"Provider is not detected correctly",
)
self.assertEqual(
self.remote_gitlab_ssh.url_protocol,
"ssh",
"Protocol is not detected correctly",
)
self.assertEqual(
self.remote_gitlab_ssh.name,
"remote_3",
"Name is not prepared correctly",
)
# -- 3 --
# Bitbucket + https
# Check if remote provider is detected correctly
self.assertEqual(
self.remote_bitbucket_https.repo_provider,
"bitbucket",
"Provider is not detected correctly",
)
self.assertEqual(
self.remote_bitbucket_https.url_protocol,
"https",
"Protocol is not detected correctly",
)
self.assertEqual(
self.remote_bitbucket_https.name,
"remote_1",
"Name is not prepared correctly",
)
# -- 4 --
# Other + ssh
# Check if remote provider is detected correctly
self.assertEqual(
self.remote_other_ssh.repo_provider,
"gitlab", # this is how giturlparse detects the provider
"Provider is not detected correctly",
)
self.assertEqual(
self.remote_other_ssh.url_protocol,
"ssh",
"Protocol is not detected correctly",
)
self.assertEqual(
self.remote_other_ssh.name,
"remote_2",
"Name is not prepared correctly",
)
def test_git_aggregator_prepare_url(self):
"""Test if url is prepared correctly"""
# -- 1 --
# GitHub + https
self.remote_github_https.repo_id.is_private = False
self.assertEqual(
self.remote_github_https._git_aggregator_prepare_url(),
self.remote_github_https.repo_id.url,
"URL is not prepared correctly",
)
# -- 2 --
# GitHub + https -> private
self.remote_github_https.repo_id.is_private = True
self.assertEqual(
self.remote_github_https._git_aggregator_prepare_url(),
"https://$GITHUB_TOKEN:x-oauth-basic@github.com/cetmix-test/cetmix-tower-test.git",
"URL is not prepared correctly",
)
# -- 3 --
# Gitlab + https
self.remote_gitlab_https.repo_id.is_private = False
self.assertEqual(
self.remote_gitlab_https._git_aggregator_prepare_url(),
self.remote_gitlab_https.repo_id.url,
"URL is not prepared correctly",
)
# -- 4 --
# Gitlab + https -> private
self.remote_gitlab_https.repo_id.is_private = True
self.assertEqual(
self.remote_gitlab_https._git_aggregator_prepare_url(),
"https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@my.gitlab.com/cetmix-test/cetmix-tower-test.git",
"URL is not prepared correctly",
)
# -- 5 --
# Bitbucket + https
self.remote_bitbucket_https.repo_id.is_private = False
self.assertEqual(
self.remote_bitbucket_https._git_aggregator_prepare_url(),
self.remote_bitbucket_https.repo_id.url,
"URL is not prepared correctly",
)
# -- 6 --
# Bitbucket + https -> private
self.remote_bitbucket_https.repo_id.is_private = True
self.assertEqual(
self.remote_bitbucket_https._git_aggregator_prepare_url(),
"https://x-token-auth:$BITBUCKET_TOKEN@bitbucket.com/cetmix-test/cetmix-tower-test-enterprise.git",
"URL is not prepared correctly",
)
# -- 7 --
# Other + ssh
self.remote_other_ssh.repo_id.is_private = False
self.assertEqual(
self.remote_other_ssh._git_aggregator_prepare_url(),
self.remote_other_ssh.repo_id.url_ssh,
"URL is not prepared correctly",
)
def test_git_aggregator_prepare_head(self):
"""Test if head is prepared correctly"""
# -- 1 --
# GitHub + PR/MR as link
self.assertEqual(
self.remote_github_https._git_aggregator_prepare_head(),
"refs/pull/123/head",
"Head is not prepared correctly",
)
# -- 2 --
# GitHub + PR/MR as number
self.remote_github_https.write({"head": "123", "head_type": "pr"})
self.assertEqual(
self.remote_github_https._git_aggregator_prepare_head(),
"refs/pull/123/head",
"Head is not prepared correctly",
)
# -- 3 --
# GitHub + branch as name
self.remote_github_https.write({"head": "main", "head_type": "branch"})
self.assertEqual(
self.remote_github_https._git_aggregator_prepare_head(),
self.remote_github_https.head,
"Head is not prepared correctly",
)
# -- 4 --
# GitHub + branch as link
self.remote_github_https.write(
{
"head": "https://github.com/cetmix-test/cetmix-tower/list/14.0-demo-branch",
"head_type": "branch",
}
)
self.assertEqual(
self.remote_github_https._git_aggregator_prepare_head(),
"14.0-demo-branch",
"Head is not prepared correctly",
)
# -- 5 --
# GitHub + commit as number
self.remote_github_https.write({"head": "1234567890", "head_type": "commit"})
self.assertEqual(
self.remote_github_https._git_aggregator_prepare_head(),
"1234567890",
"Head is not prepared correctly",
)
# -- 6 --
# GitHub + commit as link
self.remote_github_https.head = (
"https://github.com/cetmix-test/cetmix-tower/commit/1234567890"
)
self.assertEqual(
self.remote_github_https._git_aggregator_prepare_head(),
"1234567890",
"Head is not prepared correctly",
)
def test_manager_server_based_access(self):
"""Test manager access to remotes through server relationships"""
manager_remote = self.GitRemote.with_user(self.manager)
# Create a server where manager is a user
server = self.Server.create(
{
"name": "Test Server",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"os_id": self.os_debian_10.id,
"user_ids": [(4, self.manager.id)],
}
)
# Link project to server
file = self.File.create(
{
"name": "test_file",
"server_id": server.id,
}
)
self.GitProjectRel.create(
{
"server_id": server.id,
"file_id": file.id,
"git_project_id": self.project.id,
"project_format": "git_aggregator",
}
)
# Manager should be able to read remote through server relationship
remote = manager_remote.search([("id", "=", self.remote.id)])
self.assertTrue(remote)
self.assertEqual(remote.head, "main")
# Remove manager from server users
server.write({"user_ids": [(3, self.manager.id)]})
# Manager should not be able to read remote anymore
self.assertFalse(manager_remote.search([("id", "=", self.remote.id)]))
# Add manager to server managers
server.write({"manager_ids": [(4, self.manager.id)]})
# Manager should be able to read remote again
remote = manager_remote.search([("id", "=", self.remote.id)])
self.assertTrue(remote)
self.assertEqual(remote.head, "main")

View File

@@ -0,0 +1,84 @@
from odoo.exceptions import ValidationError
from .common import CommonTest
class TestRepo(CommonTest):
"""Test class for git repository."""
@classmethod
def setUpClass(cls):
super().setUpClass()
def test_repo_create_from_url_https_success(self):
"""Test if repository is created correctly"""
# -- 1 --
# Valid HTTPS URL
repo = self.Repo.create(
{
"url": "https://github.com/memes-demo/doge-memes.git",
}
)
repo.invalidate_recordset()
self.assertEqual(repo.name, "github.com/memes-demo/doge-memes")
self.assertEqual(repo.host, "github.com")
self.assertEqual(repo.owner_id.name, "memes-demo")
self.assertEqual(repo.provider, "github")
self.assertEqual(repo.is_private, False)
self.assertEqual(repo.url_ssh, "git@github.com:memes-demo/doge-memes.git")
self.assertEqual(repo.url_git, "git://github.com/memes-demo/doge-memes.git")
def test_repo_create_from_url_ssh_success(self):
"""Test if repository is created correctly"""
# -- 1 --
# Valid SSH URL
repo = self.Repo.create(
{
"url": "git@gitlab.com:chad-guy/chad-guy.git",
}
)
repo.invalidate_recordset()
self.assertEqual(repo.name, "gitlab.com/chad-guy/chad-guy")
self.assertEqual(repo.host, "gitlab.com")
self.assertEqual(repo.owner_id.name, "chad-guy")
self.assertEqual(repo.provider, "gitlab")
self.assertEqual(repo.is_private, False)
self.assertEqual(repo.url, "https://gitlab.com/chad-guy/chad-guy.git")
self.assertEqual(repo.url_git, "git://gitlab.com/chad-guy/chad-guy.git")
def test_repo_create_from_url_git_success(self):
"""Test if repository is created correctly"""
# -- 1 --
# Valid GIT URL
repo = self.Repo.create(
{
"url": "git://bitbucket.com/much-pepe/pepe-memes.git",
}
)
self.assertEqual(repo.name, "bitbucket.com/much-pepe/pepe-memes")
self.assertEqual(repo.host, "bitbucket.com")
self.assertEqual(repo.owner_id.name, "much-pepe")
self.assertEqual(repo.provider, "bitbucket")
self.assertEqual(repo.is_private, False)
self.assertEqual(repo.url_ssh, "git@bitbucket.com:much-pepe/pepe-memes.git")
self.assertEqual(repo.url, "https://bitbucket.com/much-pepe/pepe-memes.git")
def test_repo_create_from_url_fails(self):
"""Test if repository creation fails with invalid URLs"""
# Invalid URL 1
with self.assertRaises(ValidationError):
self.Repo.create(
{
"url": "something.com",
}
)
# Invalid URL 2
with self.assertRaises(ValidationError):
self.Repo.create(
{
"url": "random string",
}
)

View File

@@ -0,0 +1,416 @@
try:
from odoo.addons.queue_job.tests.common import trap_jobs
except ImportError:
trap_jobs = None
from .common import CommonTest
class TestServer(CommonTest):
"""Test setting git project to server from plan line."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.GitProjectRel.create(
{
"git_project_id": cls.git_project_1.id,
"server_id": cls.server_test_1.id,
"file_id": cls.server_1_file_1.id,
}
)
def test_server_creation_running_flight_plan(self):
"""Test that server is created with git project from plan line."""
git_project = self.GitProject.create(
{
"name": "Test Git Project",
"manager_ids": [(4, self.manager.id)],
}
)
file_template = self.FileTemplate.create(
{
"name": "Git Config Template",
"file_name": "repos.yaml",
"server_dir": "/var/test",
"code": "repositories:\n test_repo:\n "
"url: https://github.com/test/repo.git\n target: main",
}
)
command = self.Command.create(
{
"name": "Create Git Config File",
"action": "file_using_template",
"file_template_id": file_template.id,
}
)
flight_plan = self.Plan.create(
{
"name": "Git Project Setup Plan",
"note": "Sets up a git project on the server",
}
)
self.plan_line.create(
{
"plan_id": flight_plan.id,
"command_id": command.id,
"sequence": 10,
"git_project_id": git_project.id,
}
)
server_template = self.ServerTemplate.create(
{
"name": "Git Server Template",
"ssh_port": 22,
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"flight_plan_id": flight_plan.id,
"manager_ids": [(4, self.manager.id)],
}
)
action = server_template.action_create_server()
# Open the wizard and fill in the data
wizard = (
self.env["cx.tower.server.template.create.wizard"]
.with_context(**action["context"])
.create(
{
"name": "Git Server",
"ip_v4_address": "192.168.1.10",
"server_template_id": server_template.id,
"skip_host_key": True,
}
)
)
# If cetmix_tower_server_queue module is installed, test async processing
if self.env["ir.module.module"].search_count(
[("name", "=", "cetmix_tower_server_queue"), ("state", "=", "installed")]
):
with trap_jobs() as trap:
wizard.action_confirm()
# Verify that jobs were created
self.assertGreater(
len(trap.enqueued_jobs), 0, "Jobs should have been enqueued"
)
# Execute all trapped jobs to simulate async processing
trap.perform_enqueued_jobs()
else:
wizard.action_confirm()
# Now search for the created records after jobs have been executed
server = self.Server.search(
[
("name", "=", "Git Server"),
("server_template_id", "=", server_template.id),
]
)
self.assertEqual(len(server), 1, "Exactly one server should have been created")
# Verify the file was created
file = self.File.search(
[("server_id", "=", server.id), ("name", "=", "repos.yaml")]
)
self.assertEqual(
len(file), 1, "Exactly one git config file should have been created"
)
# Verify the git project relation exists
git_project_rel = self.GitProjectRel.search(
[
("server_id", "=", server.id),
("git_project_id", "=", git_project.id),
("file_id", "=", file.id),
]
)
self.assertEqual(
len(git_project_rel), 1, "Exactly one git project relation should exist"
)
self.assertEqual(
git_project_rel.file_id,
file,
"The related file should be the git config file",
)
self.assertEqual(
git_project_rel.git_project_id,
git_project,
"The related git project should match the one in the flight plan",
)
self.assertEqual(
git_project_rel.project_format,
git_project._default_project_format(),
"Project format should match the default format",
)
def test_file_creation_with_git_project_from_custom_values(self):
"""Test that git project relation is created when git project
is provided from custom values in variable_values.
"""
git_project = self.GitProject.create(
{
"name": "Test Git Project From Custom Values",
"manager_ids": [(4, self.manager.id)],
}
)
file_template = self.FileTemplate.create(
{
"name": "Git Config Template Custom Values",
"file_name": "repos_custom.yaml",
"server_dir": "/var/test",
"code": "repositories:\n test_repo:\n "
"url: https://github.com/test/repo.git\n target: main",
}
)
command = self.Command.create(
{
"name": "Create Git Config File Custom Values",
"action": "file_using_template",
"file_template_id": file_template.id,
}
)
flight_plan = self.Plan.create(
{
"name": "Git Project Setup Plan Custom Values",
"note": "Sets up a git project on the server from custom values",
}
)
# Create plan line WITHOUT git_project_id set
# The git project should come from custom values instead
plan_line = self.plan_line.create(
{
"plan_id": flight_plan.id,
"command_id": command.id,
"sequence": 10,
}
)
# Create plan log
plan_log = self.env["cx.tower.plan.log"].create(
{
"server_id": self.server_test_1.id,
"plan_id": flight_plan.id,
"plan_line_executed_id": plan_line.id,
}
)
# Create command log with variable_values containing __git_project__
command_log = self.CommandLog.create(
{
"server_id": self.server_test_1.id,
"command_id": command.id,
"plan_log_id": plan_log.id,
"variable_values": {"__git_project__": git_project.reference},
}
)
# Call the method directly to test the custom values path
file = self.server_test_1._command_runner_file_using_template_create_file(
log_record=command_log, server_dir="/var/test"
)
# Verify the file was created
self.assertTrue(file, "File should have been created")
# Verify the git project relation exists
git_project_rel = self.GitProjectRel.search(
[
("server_id", "=", self.server_test_1.id),
("git_project_id", "=", git_project.id),
("file_id", "=", file.id),
]
)
self.assertEqual(
len(git_project_rel), 1, "Exactly one git project relation should exist"
)
self.assertEqual(
git_project_rel.file_id,
file,
"The related file should be the git config file",
)
self.assertEqual(
git_project_rel.git_project_id,
git_project,
"The related git project should match the one from custom values",
)
self.assertEqual(
git_project_rel.project_format,
git_project._default_project_format(),
"Project format should match the default format",
)
def test_file_creation_with_git_project_from_custom_values_priority(self):
"""Test that git project from custom values takes priority
over git project from plan line.
"""
git_project_custom = self.GitProject.create(
{
"name": "Test Git Project From Custom Values Priority",
"manager_ids": [(4, self.manager.id)],
}
)
git_project_plan_line = self.GitProject.create(
{
"name": "Test Git Project From Plan Line",
"manager_ids": [(4, self.manager.id)],
}
)
file_template = self.FileTemplate.create(
{
"name": "Git Config Template Priority",
"file_name": "repos_priority.yaml",
"server_dir": "/var/test",
"code": "repositories:\n test_repo:\n "
"url: https://github.com/test/repo.git\n target: main",
}
)
command = self.Command.create(
{
"name": "Create Git Config File Priority",
"action": "file_using_template",
"file_template_id": file_template.id,
}
)
flight_plan = self.Plan.create(
{
"name": "Git Project Setup Plan Priority",
"note": "Tests priority of custom values over plan line",
}
)
# Create plan line WITH git_project_id set
# But custom values should take priority
plan_line = self.plan_line.create(
{
"plan_id": flight_plan.id,
"command_id": command.id,
"sequence": 10,
"git_project_id": git_project_plan_line.id,
}
)
# Create plan log
plan_log = self.env["cx.tower.plan.log"].create(
{
"server_id": self.server_test_1.id,
"plan_id": flight_plan.id,
"plan_line_executed_id": plan_line.id,
}
)
# Create command log with variable_values containing __git_project__
# This should take priority over plan_line.git_project_id
command_log = self.CommandLog.create(
{
"server_id": self.server_test_1.id,
"command_id": command.id,
"plan_log_id": plan_log.id,
"variable_values": {"__git_project__": git_project_custom.reference},
}
)
# Call the method directly to test the custom values path
file = self.server_test_1._command_runner_file_using_template_create_file(
log_record=command_log, server_dir="/var/test"
)
# Verify the file was created
self.assertTrue(file, "File should have been created")
# Verify the git project relation uses the git project from custom values
# (not the one from plan line)
git_project_rel = self.GitProjectRel.search(
[
("server_id", "=", self.server_test_1.id),
("git_project_id", "=", git_project_custom.id),
("file_id", "=", file.id),
]
)
self.assertEqual(
len(git_project_rel), 1, "Exactly one git project relation should exist"
)
self.assertEqual(
git_project_rel.git_project_id,
git_project_custom,
"The related git project should match the one from custom values, "
"not from plan line",
)
# Verify that the plan line git project was NOT used
git_project_rel_plan_line = self.GitProjectRel.search(
[
("server_id", "=", self.server_test_1.id),
("git_project_id", "=", git_project_plan_line.id),
("file_id", "=", file.id),
]
)
self.assertEqual(
len(git_project_rel_plan_line),
0,
"No relation should exist for the plan line git project",
)
def test_server_get_servers_by_git_ref_success(self):
"""Check the success case of server.get_servers_by_git_ref"""
# 1. URL only
servers = self.Server.get_servers_by_git_ref(
self.remote_github_https.repo_id.url
)
self.assertEqual(servers, self.server_test_1)
# 2. Specific URL with specific head
servers = self.Server.get_servers_by_git_ref(
self.remote_github_https.repo_id.url, "123"
)
self.assertEqual(servers, self.server_test_1)
# 2. Specific URL with specific head and head type
servers = self.Server.get_servers_by_git_ref(
self.remote_github_https.repo_id.url, "123", "pr"
)
self.assertEqual(servers, self.server_test_1)
def test_server_get_servers_by_git_ref_no_match(self):
"""Check the no match case of server.get_servers_by_git_ref"""
# 1. Repo link does not exist
servers = self.Server.get_servers_by_git_ref(
"https://github.com/other-org/other-repo.git", "main", "branch"
)
self.assertFalse(servers)
# 2. Repo link exists, but remote does not exist
servers = self.Server.get_servers_by_git_ref(
self.repo_cetmix_tower.url, "3311", "pr"
)
self.assertFalse(servers)
# 3. Repo link exists, but remote type does not exist
servers = self.Server.get_servers_by_git_ref(
self.repo_cetmix_tower.url, "main", "commit"
)
self.assertFalse(servers)

View File

@@ -0,0 +1,226 @@
from odoo.exceptions import AccessError
from .common import CommonTest
class TestSource(CommonTest):
"""Test class for git source."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create another manager for testing
cls.manager_2 = cls.Users.create(
{
"name": "Second Manager",
"login": "manager2",
"email": "manager2@test.com",
"groups_id": [(4, cls.env.ref("cetmix_tower_server.group_manager").id)],
}
)
# Create test project and source as root
cls.project = cls.GitProject.create(
{
"name": "Test Project",
}
)
cls.source = cls.GitSource.create(
{
"name": "Test Source",
"git_project_id": cls.project.id,
}
)
def test_user_access(self):
"""Test that regular users have no access to git sources"""
user_source = self.GitSource.with_user(self.user)
# Test CRUD operations
with self.assertRaises(AccessError):
user_source.create(
{
"name": "New Source",
"git_project_id": self.project.id,
}
)
with self.assertRaises(AccessError):
user_source.browse(self.source.id).read(["name"])
with self.assertRaises(AccessError):
user_source.browse(self.source.id).write({"name": "Updated Name"})
with self.assertRaises(AccessError):
user_source.browse(self.source.id).unlink()
def test_manager_read_access(self):
"""Test manager read access rules"""
manager_source = self.GitSource.with_user(self.manager)
# Manager not in project user_ids or manager_ids - should not read
with self.assertRaises(AccessError):
manager_source.browse(self.source.id).read(["name"])
# Add manager to project user_ids - should read
self.project.write({"user_ids": [(4, self.manager.id)]})
self.assertEqual(manager_source.browse(self.source.id).name, "Test Source")
# Remove from user_ids, add to manager_ids - should read
self.project.write(
{"user_ids": [(3, self.manager.id)], "manager_ids": [(4, self.manager.id)]}
)
self.assertEqual(manager_source.browse(self.source.id).name, "Test Source")
def test_manager_write_access(self):
"""Test manager write/create access rules"""
manager_source = self.GitSource.with_user(self.manager)
# Create project as manager - should be added to manager_ids automatically
project = self.GitProject.with_user(self.manager).create(
{
"name": "Manager Project",
}
)
self.assertIn(self.manager, project.manager_ids)
# Create source in own project - should succeed
new_source = manager_source.create(
{
"name": "New Source",
"git_project_id": project.id,
}
)
self.assertTrue(new_source.exists())
# Write to own source - should succeed
new_source.write({"name": "Updated Name"})
self.assertEqual(new_source.name, "Updated Name")
# Write to other's source - should fail
with self.assertRaises(AccessError):
manager_source.browse(self.source.id).write({"name": "Updated Name"})
def test_manager_unlink_access(self):
"""Test manager unlink access rules"""
# Create project and source as manager_2
project = self.GitProject.with_user(self.manager_2).create(
{
"name": "Manager 2 Project",
}
)
source = self.GitSource.with_user(self.manager_2).create(
{
"name": "Source to Delete",
"git_project_id": project.id,
}
)
manager_source = self.GitSource.with_user(self.manager)
# Try delete as different manager - should fail even if added to manager_ids
project.write({"manager_ids": [(4, self.manager.id)]})
with self.assertRaises(AccessError):
manager_source.browse(source.id).unlink()
# Create source as manager and try delete - should succeed
own_source = manager_source.create(
{
"name": "Own Source",
"git_project_id": project.id,
}
)
self.assertTrue(own_source.exists())
own_source.unlink()
self.assertFalse(own_source.exists())
def test_root_access(self):
"""Test root access rules"""
root_source = self.GitSource.with_user(self.root)
# Create
new_source = root_source.create(
{
"name": "Root Source",
"git_project_id": self.project.id,
}
)
self.assertTrue(new_source.exists())
# Read
self.assertEqual(root_source.browse(self.source.id).name, "Test Source")
# Write
root_source.browse(self.source.id).write({"name": "Updated by Root"})
self.assertEqual(self.source.name, "Updated by Root")
# Delete
new_source.unlink()
self.assertFalse(new_source.exists())
def test_source_git_aggregator_prepare_record(self):
"""Test if source prepare record method works correctly."""
# -- 1 --
# Source 1
expected_result = {
"remotes": {
"remote_1": "https://github.com/cetmix-test/cetmix-tower-test.git",
"remote_2": "https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@my.gitlab.com/cetmix-test/cetmix-tower-test.git",
"remote_3": "git@my.gitlab.com:cetmix-test/cetmix-tower-test.git",
},
"merges": [
{"remote": "remote_1", "ref": "refs/pull/123/head"},
{"remote": "remote_2", "ref": "main"},
{"remote": "remote_3", "ref": "10000000"},
],
"target": "remote_1",
}
prepared_result = self.git_source_1._git_aggregator_prepare_record()
self.assertEqual(
prepared_result, expected_result, "Prepared result is not correct"
)
def test_manager_server_based_access(self):
"""Test manager access to sources through server relationships"""
manager_source = self.GitSource.with_user(self.manager)
# Create a server where manager is a user
server = self.Server.create(
{
"name": "Test Server",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"os_id": self.os_debian_10.id,
"user_ids": [(4, self.manager.id)],
}
)
# Link project to server
file = self.File.create(
{
"name": "test_file",
"server_id": server.id,
}
)
self.GitProjectRel.create(
{
"server_id": server.id,
"file_id": file.id,
"git_project_id": self.project.id,
"project_format": "git_aggregator",
}
)
# Manager should be able to read source through server relationship
self.assertEqual(manager_source.browse(self.source.id).name, "Test Source")
# Remove manager from server users
server.write({"user_ids": [(3, self.manager.id)]})
# Manager should not be able to read source anymore
with self.assertRaises(AccessError):
manager_source.browse(self.source.id).read(["name"])
# Add manager to server managers
server.write({"manager_ids": [(4, self.manager.id)]})
# Manager should be able to read source again
self.assertEqual(manager_source.browse(self.source.id).name, "Test Source")

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="cx_tower_file_template_view_form" model="ir.ui.view">
<field name="name">cx.tower.file.template.view.form</field>
<field name="model">cx.tower.file.template</field>
<field
name="inherit_id"
ref="cetmix_tower_server.cx_tower_file_template_view_form"
/>
<field name="arch" type="xml">
<field name="source" position="after">
<field name="git_project_id" invisible="not git_project_id" />
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="cx_tower_file_view_form" model="ir.ui.view">
<field name="name">cx.tower.file.view.form</field>
<field name="model">cx.tower.file</field>
<field name="inherit_id" ref="cetmix_tower_server.cx_tower_file_view_form" />
<field name="arch" type="xml">
<field name="auto_sync" position="before">
<field name="git_project_id" invisible="not git_project_id" />
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,291 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- List View -->
<record id="cx_tower_git_project_view_tree" model="ir.ui.view">
<field name="name">cx.tower.git.project.list</field>
<field name="model">cx.tower.git.project</field>
<field name="arch" type="xml">
<list>
<header>
<button
type="object"
icon="fa-floppy-o"
title="Save records as YAML"
name="action_open_yaml_export_wizard"
string="Export YAML"
/>
</header>
<field name="name" />
<field
name="server_ids"
widget="many2many_tags"
options="{'color_field': 'color'}"
/>
<field name="active" widget="boolean_toggle" />
</list>
</field>
</record>
<!-- Form View -->
<record id="cx_tower_git_project_view_form" model="ir.ui.view">
<field name="name">cx.tower.git.project.form</field>
<field name="model">cx.tower.git.project</field>
<field name="arch" type="xml">
<form>
<header>
<button
type="object"
icon="fa-floppy-o"
title="Save record as YAML"
name="action_open_yaml_export_wizard"
string="Export YAML"
/>
</header>
<sheet>
<widget
name="web_ribbon"
title="Archived"
bg_color="bg-danger"
invisible="active"
/>
<div class="oe_title">
<h1>
<field name="name" placeholder="Name" />
</h1>
<h3>
<field
name="reference"
placeholder="Reference. Can contain English letters, digits and '_'. Leave blank to autogenerate"
/>
</h3>
</div>
<group>
<group string="General">
<field name="active" invisible="1" />
<field
name="server_ids"
widget="many2many_tags"
invisible="not server_ids"
options="{'color_field': 'color'}"
/>
<field
name="file_template_ids"
widget="many2many_tags"
invisible="not file_template_ids"
/>
</group>
<group string="Git Aggregator">
<field
name="git_aggregator_root_dir"
string="Root Directory"
placeholder="Git aggregator root directory where sources will be cloned. Leave blank to use '.'"
/>
</group>
<field name="note" placeholder="Put your notes here..." />
</group>
<notebook>
<page name="sources" string="Sources">
<field name="source_ids">
<list
decoration-muted="not enabled"
decoration-info="remote_count_private == remote_count and remote_count &gt; 0"
decoration-warning="remote_count_private != remote_count and remote_count &gt; 0 and remote_count_private &gt; 0"
>
<field name="sequence" widget="handle" />
<field
name="name"
placeholder="..to be autogenerated"
/>
<field name="remote_count" optional="show" />
<field
name="remote_count_private"
optional="hide"
/>
<field name="enabled" widget="boolean_toggle" />
<field name="reference" optional="hide" />
<field name="create_uid" optional="hide" />
</list>
</field>
<field name="has_private_remotes" invisible="1" />
<field name="has_partially_private_remotes" invisible="1" />
<div class="text-info" invisible="not has_private_remotes">
<p>
* Sources where all remotes are private
</p>
</div>
<div
class="text-warning"
invisible="not has_partially_private_remotes"
>
<p>
* Sources where some remotes are private
</p>
</div>
</page>
<page name="files" string="Files">
<field name="git_project_rel_ids">
<list editable="bottom">
<field
name="server_id"
options="{'no_create': True, 'no_create_edit': True}"
/>
<field
name="file_id"
options="{'no_create': True, 'no_create_edit': True}"
/>
<field name="project_format" />
<field name="auto_sync" />
<button
type="object"
name="action_open_server"
string="Open Server"
title="Open Server"
class="btn-secondary"
/>
</list>
<form>
<group>
<field name="server_id" />
<field name="file_id" />
<field name="project_format" />
<field name="auto_sync" />
</group>
</form>
</field>
</page>
<page name="file_templates" string="File Templates">
<field name="git_project_file_template_rel_ids">
<list editable="bottom">
<field
name="file_template_id"
options="{'no_create': True, 'no_create_edit': True}"
/>
<field name="project_format" />
<button
type="object"
string="Open file template"
name="action_open_file_template"
title="Open File Template"
class="btn-secondary"
/>
</list>
<form>
<group>
<field name="file_template_id" />
<field name="project_format" />
</group>
</form>
</field>
</page>
<page
name="repos"
string="Repos"
groups="base.group_no_one"
invisible="not repo_ids"
>
<field name="repo_ids" />
</page>
<page
name="access"
string="Access"
groups="cetmix_tower_server.group_manager"
>
<group name="access">
<field
name="user_ids"
widget="many2many_tags"
placeholder="users who can view this record"
options="{'no_create': True}"
/>
<field
name="manager_ids"
widget="many2many_tags"
placeholder="managers who can modify this record"
options="{'no_create': True}"
/>
</group>
<div
class="alert alert-warning"
role="alert"
style="margin-bottom:0px;"
>
<ul>
<li>
<b
>Users.</b> All users who have "Manager" group and are either set in "Users" or in "Managers" in <b
>
<u>all</u>
</b> related servers.
</li>
<li>
<b
>Managers.</b> All users who have "Manager" group and are set as "Managers" in <b
>
<u>all</u>
</b> related servers.
This is done to avoid unpredictable consequences when some of the servers are not updated due to access restrictions when a project is updated.
</li>
</ul>
You can edit these fields at your own risk. However keep in mind that they will be automatically updated each time related servers are added, removed or updated.
</div>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="cx_tower_git_project_view_search" model="ir.ui.view">
<field name="name">cx.tower.git.project.view.search</field>
<field name="model">cx.tower.git.project</field>
<field name="arch" type="xml">
<search>
<field
name="name"
string="Name/Reference"
filter_domain="['|', ('name', 'ilike', self), ('reference', 'ilike', self)]"
/>
<field name="server_ids" />
<filter
string="Active"
name="active"
domain="[('active', '=', True)]"
/>
<filter
string="Archived"
name="archived"
domain="[('active', '=', False)]"
/>
<separator />
<filter
string="Has Servers"
name="has_servers"
domain="[('server_ids', '!=', False)]"
/>
<filter
string="No Servers"
name="no_servers"
domain="[('server_ids', '=', False)]"
/>
</search>
</field>
</record>
<!-- Action -->
<record id="cx_tower_git_project_action" model="ir.actions.act_window">
<field name="name">Git Projects</field>
<field name="path">cetmix_tower_git_projects</field>
<field name="res_model">cx.tower.git.project</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first git project!
</p>
<p>
Git projects represent collections of git repositories with their metadata and configuration.
They are used in deployment processes to define the source code to be deployed.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- List View -->
<record id="cx_tower_git_remote_view_tree" model="ir.ui.view">
<field name="name">cx.tower.git.remote.list</field>
<field name="model">cx.tower.git.remote</field>
<field name="arch" type="xml">
<list decoration-info="is_private">
<field
name="repo_id"
placeholder="select or enter a link"
options="{'no_create_edit': True}"
/>
<field name="is_private" optional="hide" />
<field name="head_type" />
<field name="head" />
<field name="enabled" widget="boolean_toggle" />
<field name="create_uid" optional="hide" groups="base.group_no_one" />
<field name="reference" optional="hide" groups="base.group_no_one" />
</list>
</field>
</record>
<!-- Form View -->
<record id="cx_tower_git_remote_view_form" model="ir.ui.view">
<field name="name">cx.tower.git.remote.form</field>
<field name="model">cx.tower.git.remote</field>
<field name="arch" type="xml">
<form>
<sheet>
<widget
name="web_ribbon"
title="Disabled"
bg_color="bg-danger"
invisible="enabled"
/>
<group>
<field name="sequence" groups="base.group_no_one" />
<field name="enabled" />
<field name="active" invisible="1" />
<field
name="repo_id"
placeholder="select or enter a link"
options="{'no_create_edit': True}"
/>
<field name="is_private" />
<field
name="url_protocol"
widget="radio"
options="{'horizontal': true}"
/>
<field
name="head_type"
widget="radio"
options="{'horizontal': true}"
/>
<field
name="head"
invisible="not head_type"
placeholder="Branch/PR/commit number or link"
/>
<field name="create_uid" groups="base.group_no_one" />
</group>
</sheet>
</form>
</field>
</record>
<!-- Action -->
<record id="action_cx_tower_git_remote" model="ir.actions.act_window">
<field name="name">Git Remotes</field>
<field name="res_model">cx.tower.git.remote</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first git remote!
</p>
<p>
Git remotes represent branches, pull requests, or commits from git repositories.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- List View -->
<record id="cx_tower_git_repo_owner_view_tree" model="ir.ui.view">
<field name="name">cx.tower.git.repo.owner.list</field>
<field name="model">cx.tower.git.repo.owner</field>
<field name="arch" type="xml">
<list>
<field name="display_name" />
<field name="name" />
</list>
</field>
</record>
<!-- Form View -->
<record id="cx_tower_git_repo_owner_view_form" model="ir.ui.view">
<field name="name">cx.tower.git.repo.owner.form</field>
<field name="model">cx.tower.git.repo.owner</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="display_name" placeholder="e.g., Cetmix, OCA" />
<field name="name" placeholder="e.g., cetmix, oca" />
<field
name="reference"
placeholder="Can contain English letters, digits and '_'. Leave blank to autogenerate"
/>
<field name="secret_id" />
</group>
<notebook>
<page string="Repositories" name="repositories">
<field name="repo_ids" readonly="True">
<list>
<field name="name" />
<field name="reference" optional="hide" />
<field name="provider" />
<field name="is_private" />
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Action -->
<record id="action_cx_tower_git_repo_owner" model="ir.actions.act_window">
<field name="name">Repository Owners</field>
<field name="res_model">cx.tower.git.repo.owner</field>
<field name="path">cetmix_tower_git_repo_owners</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first repository owner!
</p>
<p>
Repository owners represent organizations or users that own git repositories.
Examples include "cetmix", "OCA", etc.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- List View -->
<record id="cx_tower_git_repo_view_tree" model="ir.ui.view">
<field name="name">cx.tower.git.repo.list</field>
<field name="model">cx.tower.git.repo</field>
<field name="arch" type="xml">
<list decoration-info="is_private">
<field name="name" />
<field name="reference" optional="hide" />
<field name="provider" optional="show" />
<field name="owner_id" optional="hide" />
<field name="is_private" optional="hide" />
<field name="remote_count" optional="hide" />
<field name="git_project_count" optional="hide" />
</list>
</field>
</record>
<!-- Form View -->
<record id="cx_tower_git_repo_view_form" model="ir.ui.view">
<field name="name">cx.tower.git.repo.form</field>
<field name="model">cx.tower.git.repo</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button
name="action_view_remotes"
type="object"
string="Remotes"
class="oe_stat_button"
icon="fa-external-link"
invisible="remote_count == 0"
>
<field
name="remote_count"
widget="statinfo"
string="Remotes"
/>
</button>
<button
name="action_view_projects"
type="object"
string="GitProjects"
class="oe_stat_button"
icon="fa-folder"
invisible="git_project_count == 0"
>
<field
name="git_project_count"
widget="statinfo"
string="Projects"
/>
</button>
</div>
<group>
<group name="info">
<field
name="url"
placeholder="https, ssh or git formats are accepted"
/>
<field name="url_ssh" />
<field name="url_git" />
<field name="repo" placeholder="e.g., cetmix-tower, odoo" />
<field name="host" />
</group>
<group name="details">
<field
name="reference"
placeholder="Can contain English letters, digits and '_'. Leave blank to autogenerate"
/>
<field name="active" />
<field name="owner_id" />
<field name="provider" />
<field name="is_private" />
<field name="secret_id" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="cx_tower_git_repo_view_search" model="ir.ui.view">
<field name="name">cx.tower.git.repo.search</field>
<field name="model">cx.tower.git.repo</field>
<field name="arch" type="xml">
<search>
<field
name="repo"
string="Name/Reference"
filter_domain="['|', ('repo', 'ilike', self), ('reference', 'ilike', self)]"
/>
<field name="owner_id" string="Org" />
<field name="provider" />
<filter
string="Public"
name="public"
domain="[('is_private', '=', False)]"
/>
<filter
string="Private"
name="private"
domain="[('is_private', '=', True)]"
/>
<separator />
<filter
string="Provider: Other"
name="no_provider"
domain="[('provider', '=', 'other')]"
/>
<group expand="0" string="Group By">
<filter
string="Provider"
name="group_provider"
context="{'group_by': 'provider'}"
/>
<filter
string="Org"
name="group_owner"
context="{'group_by': 'owner_id'}"
/>
</group>
<searchpanel>
<field
name="provider"
string="Provider"
icon="fa-globe"
enable_counters="1"
/>
<field
name="owner_id"
string="Org"
icon="fa-building"
enable_counters="1"
/>
</searchpanel>
</search>
</field>
</record>
<!-- Action -->
<record id="action_cx_tower_git_repo" model="ir.actions.act_window">
<field name="name">Repositories</field>
<field name="path">cetmix_tower_git_repositories</field>
<field name="res_model">cx.tower.git.repo</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first repository!
</p>
<p>
Repositories represent git repositories with their metadata and configuration.
They can be linked to remotes to automatically populate URL information.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- List View -->
<record id="cx_tower_git_source_view_tree" model="ir.ui.view">
<field name="name">cx.tower.git.source.list</field>
<field name="model">cx.tower.git.source</field>
<field name="arch" type="xml">
<list>
<field name="name" />
<field name="git_project_id" />
</list>
</field>
</record>
<!-- Form View -->
<record id="cx_tower_git_source_view_form" model="ir.ui.view">
<field name="name">cx.tower.git.source.form</field>
<field name="model">cx.tower.git.source</field>
<field name="arch" type="xml">
<form>
<sheet>
<widget
name="web_ribbon"
title="Disabled"
bg_color="bg-danger"
invisible="enabled"
/>
<div class="oe_title">
<h1>
<field
name="name"
placeholder="Name. Leave blank to autogenerate"
/>
</h1>
<h3>
<field
name="reference"
placeholder="Reference. Can contain English letters, digits and '_'. Leave blank to autogenerate"
invisible="not reference"
groups="base.group_no_one"
/>
</h3>
</div>
<group>
<field name="sequence" groups="base.group_no_one" />
<field name="enabled" />
<field name="active" invisible="1" />
</group>
<notebook>
<page name="remotes" string="Remotes">
<div
class="alert alert-warning"
role="alert"
style="margin-bottom:0px;"
invisible="remote_count &lt; 2"
>
<p>
The top one remote will be used as a merge target.
You can re-arrange remotes by dragging them or changing their sequence value.
</p>
</div>
<field name="remote_count" invisible="1" />
<field name="remote_ids">
<list
decoration-muted="not enabled"
decoration-info="is_private"
>
<field name="sequence" widget="handle" />
<field name="repo_id" />
<field name="head_type" />
<field name="head" />
<field name="url_protocol" />
<field name="enabled" widget="boolean_toggle" />
<field name="is_private" optional="hide" />
<field
name="reference"
optional="hide"
groups="base.group_no_one"
/>
<field
name="create_uid"
optional="hide"
groups="base.group_no_one"
/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="cx_tower_plan_line_view_form" model="ir.ui.view">
<field name="name">cx.tower.plan.line.view.form</field>
<field name="model">cx.tower.plan.line</field>
<field
name="inherit_id"
ref="cetmix_tower_server.cx_tower_plan_line_view_form"
/>
<field name="arch" type="xml">
<xpath expr="//group[@name='main']" position="after">
<group invisible="not file_template_id">
<field name="git_project_id" />
<field name="is_make_copy" invisible="not git_project_id" />
</group>
<div
class="alert alert-info"
role="alert"
invisible="not file_template_id"
>
<p>
You can also provide a Git Project reference using the <code
>__git_project__</code> variable in the flight plan custom values.<br
/>
Python command code example:
<code>
custom_values['__git_project__'] = 'my_git_project'
</code>
<br />
Important: if defined, this variable value overrides the Git Project selected in the form.
</p>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="cx_tower_server_view_form" model="ir.ui.view">
<field name="name">cx.tower.server.view.form.shortcuts</field>
<field name="model">cx.tower.server</field>
<field name="inherit_id" ref="cetmix_tower_server.cx_tower_server_view_form" />
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page
name="git_projects"
string="Git Projects"
groups="cetmix_tower_server.group_manager,cetmix_tower_server.group_root"
>
<field name="git_project_rel_ids">
<list editable="bottom">
<field name="server_id" column_invisible="True" />
<field name="git_project_id" />
<field name="file_id" />
<field name="project_format" />
<field name="auto_sync" />
<button
type="object"
string="Configure"
name="action_open_project"
title="Open Git Project"
class="btn-secondary"
/>
</list>
<form>
<group>
<field name="server_id" invisible="1" />
<field name="git_project_id" />
<field name="file_id" />
<field name="project_format" />
<field name="auto_sync" />
</group>
</form>
</field>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,32 @@
<odoo>
<!-- Git Projects -> Tools -->
<menuitem
id="menu_cx_tower_git_project"
name="Git Projects"
action="cx_tower_git_project_action"
sequence="30"
parent="cetmix_tower_server.menu_tools"
/>
<!-- Git Projects Settings -> Settings -->
<menuitem
id="menu_cx_tower_git_project_settings"
name="Git Projects"
parent="cetmix_tower_server.menu_settings"
sequence="60"
/>
<menuitem
id="menu_cx_tower_git_repositories"
name="Repositories"
parent="menu_cx_tower_git_project_settings"
action="action_cx_tower_git_repo"
sequence="10"
/>
<menuitem
id="menu_cx_tower_git_repository_owners"
name="Repository Owners"
parent="menu_cx_tower_git_project_settings"
action="action_cx_tower_git_repo_owner"
sequence="20"
/>
</odoo>

View File

@@ -0,0 +1,263 @@
================
Cetmix Tower OVH
================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:857636ba9899c2980b8ee24c3cf12b3854a7a31704c39395c7f81648d1b3ca57
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github
:target: https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_ovh
:alt: cetmix/cetmix-tower
|badge1| |badge2| |badge3|
This module integrates the OVH Python client, enabling seamless
interaction with OVH cloud services within the Cetmix Tower.
**Table of contents**
.. contents::
:local:
Use Cases / Context
===================
Although OVH allows API calls without using an SDK, we found that
integrating the OVH SDK into Cetmix Tower makes provisioning,
configuring, and maintaining OVH instances more convenient for the end
user. However, not all Cetmix Tower users require this functionality, so
to avoid overloading the system, we have included it in a separate
module.
Configuration
=============
Setting up OVH Access
=====================
Create OVH API Credentials
--------------------------
To use the OVH integration with Cetmix Tower, you need to create OVH API
credentials:
- Follow the `official OVH
documentation <https://docs.ovh.com/gb/en/api/first-steps/>`__
(https://docs.ovh.com/gb/en/api/first-steps/) for creating an
application and generating API keys
- It's recommended to create a dedicated application with appropriate
permissions for Cetmix Tower
- Store your Application Key, Application Secret, and Consumer Key
securely—you'll need them in the next step
Configure OVH Secrets in Cetmix Tower
-------------------------------------
Create three secrets in Cetmix Tower to store your OVH credentials:
- Navigate to ``Cetmix Tower > Settings > Keys and Secrets``
- Create a new Secret with:
- Name: ``OVH Application Key``
- Reference: ``ovh_application_key``
- Key Type: ``Secret``
- Enter your OVH Application Key in the Secret Value tab
- Similarly, create another Secret with:
- Name: ``OVH Application Secret``
- Reference: ``ovh_application_secret``
- Key Type: ``Secret``
- Enter your OVH Application Secret in the Secret Value tab
- Finally, create a Secret with:
- Name: ``OVH Consumer Key``
- Reference: ``ovh_consumer_key``
- Key Type: ``Secret``
- Enter your OVH Consumer Key in the Secret Value tab
..
Note: These secrets will be accessible as
``#!cxtower.secret.ovh_application_key!#``,
``#!cxtower.secret.ovh_application_secret!#``, and
``#!cxtower.secret.ovh_consumer_key!#`` in your commands.
Configure OVH Endpoint
----------------------
Create a variable to define your OVH API endpoint (region):
- Navigate to ``Cetmix Tower > Settings > Variables``
- Create a new Variable with:
- Name: ``OVH Endpoint``
- Reference: ``ovh_endpoint``
- Type: ``String``
- Set your OVH endpoint (e.g., ``ovh-eu``, ``ovh-ca``, ``ovh-us``) as
the value
Usage
=====
Cetmix Tower OVHcloud Command Usage
===================================
**Disclaimer**: The following example demonstrates one of many
possible commands you can create and run with this module. The
``ovh`` Python library provides access to the full range of OVHcloud
APIs this is just a starting point to help you get familiar with
the integration.
Example of Cetmix Tower Python Command to Create DNS Records
------------------------------------------------------------
- **Navigate to Command Creation**
- Go to ``Cetmix Tower > Commands > Commands``
- Click the ``Create`` button
- **Configure Command Settings**
- Set a descriptive ``Name`` (e.g., "List OVHcloud Instances")
- Leave ``Reference`` blank to generate automatically (or set a custom
reference)
- Select ``Action``: "Execute Python code"
- Set ``Access Level``: Choose appropriate level (e.g., "Manager")
- Optional: Set ``Default Path`` if needed
- Optional: Add ``Tags`` (e.g., "ovh", "cloud", "instance") for better
organization
- **Add Required Variables**
- In the ``Variables`` tab, add the previously configured variable:
- ``ovh_endpoint`` (e.g., "ovh-eu")
- **Add Required Secrets**
- In the ``Secrets`` field, add the previously configured secrets:
- ``ovh_application_key``
- ``ovh_application_secret``
- ``ovh_consumer_key``
- **Write Python Code**
- Go to the ``Code`` tab
- Enter the following Python code:
.. code:: python
# List OVHcloud instances using ovh API
result = {"exit_code": 0, "message": None}
client = ovh.Client(
endpoint={{ ovh_endpoint }},
application_key=#!cxtower.secret.ovh_application_key!#,
application_secret=#!cxtower.secret.ovh_application_secret!#,
consumer_key=#!cxtower.secret.ovh_consumer_key!#
)
# Required variables:
# - domain_name: The main domain (e.g., "example.com")
# - subdomain: The subdomain to create (e.g., "test")
try:
# Create a new subdomain by adding a DNS entry (A record as example)
ip_address = "1.2.3.4" # Replace with the desired IP address
response = client.post(
"/domain/zone/" + domain_name + "/record",
fieldType="A",
subDomain=subdomain,
target=ip_address,
ttl=3600
)
# Refresh the zone to apply changes
client.post("/domain/zone/" + domain_name + "/refresh")
result["message"] = "Subdomain '" + subdomain + "." + domain_name + "' created and DNS zone refreshed."
except Exception as e:
result["exit_code"] = 1
result["message"] = "Error: " + str(e)
- **Save the Command**
- Click the ``Save`` button to create the command
Running the OVHcloud Instance Command
-------------------------------------
- **Navigate to Server**
- Go to ``Cetmix Tower > Servers > Servers``
- Open the server where you want to run the command
- **Execute Command from Server**
- Click the ``Command`` button at the top of the server form
- In the popup dialog:
- Select your OVHcloud instance command from the dropdown
- Verify the variable values (if any need adjustment)
- Click ``Run`` to execute
- **View Command Results**
- After execution, the command log will display showing:
- The command executed
- Execution status
- Output message containing OVHcloud instance details if successful
Changelog
=========
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/cetmix/cetmix-tower/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_ovh%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Cetmix
* Giovanni Serra
Maintainers
-----------
.. |maintainer-GSLabIt| image:: https://github.com/GSLabIt.png?size=40px
:target: https://github.com/GSLabIt
:alt: GSLabIt
Current maintainer:
|maintainer-GSLabIt|
This module is part of the `cetmix/cetmix-tower <https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_ovh>`_ project on GitHub.
You are welcome to contribute.

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,24 @@
# Copyright 2024 Cetmix OÜ
# Copyright 2025 Giovanni Serra <giovanni@gslab.it>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Cetmix Tower OVH",
"summary": """Cetmix Tower OVH API integration""",
"version": "18.0.1.0.1",
"category": "Productivity",
"license": "AGPL-3",
"author": "Cetmix, Giovanni Serra",
"maintainers": ["GSLabIt"],
"website": "https://tower.cetmix.com",
"images": ["static/description/banner.png"],
"application": False,
"installable": True,
"demo": [
"demo/demo_data.xml",
],
"external_dependencies": {"python": ["ovh"]},
"depends": [
"cetmix_tower_server",
],
}

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<!-- ===============================================
Keys
================================================ -->
<record id="ovh_application_key" model="cx.tower.key">
<field name="name">OVH Application Key</field>
<field name="reference">ovh_application_key</field>
<field name="key_type">s</field>
<field name="secret_value" />
</record>
<record id="ovh_application_secret" model="cx.tower.key">
<field name="name">OVH Application Secret</field>
<field name="reference">ovh_application_secret</field>
<field name="key_type">s</field>
<field name="secret_value" />
</record>
<record id="ovh_consumer_key" model="cx.tower.key">
<field name="name">OVH Consumer Key</field>
<field name="reference">ovh_consumer_key</field>
<field name="key_type">s</field>
<field name="secret_value" />
</record>
<!-- ===============================================
Variables
================================================ -->
<record id="variable_ovh_endpoint" model="cx.tower.variable">
<field name="name">OVH Endpoint</field>
<field name="reference">ovh_endpoint</field>
<field name="variable_type">o</field>
</record>
<record id="variable_ovh_zone_name" model="cx.tower.variable">
<field name="name">OVH Zone Name</field>
<field name="reference">ovh_zone_name</field>
</record>
<!-- ===============================================
Global Variable values
================================================ -->
<record id="value_ovh_zone_name" model="cx.tower.variable.value">
<field name="variable_id" ref="variable_ovh_zone_name" />
<field name="value_char">your_zone.name</field>
</record>
<record id="option_ovh_endpoint_eu" model="cx.tower.variable.option">
<field name="variable_id" ref="variable_ovh_endpoint" />
<field name="name">ovh-eu</field>
<field name="reference">ovh-eu</field>
<field name="value_char">ovh-eu</field>
<field name="sequence">10</field>
</record>
<record id="option_ovh_endpoint_us" model="cx.tower.variable.option">
<field name="variable_id" ref="variable_ovh_endpoint" />
<field name="name">ovh-us</field>
<field name="reference">ovh-us</field>
<field name="value_char">ovh-us</field>
<field name="sequence">20</field>
</record>
<record id="option_ovh_endpoint_ca" model="cx.tower.variable.option">
<field name="variable_id" ref="variable_ovh_endpoint" />
<field name="name">ovh-ca</field>
<field name="reference">ovh-ca</field>
<field name="value_char">ovh-ca</field>
<field name="sequence">30</field>
</record>
<record id="option_ovh_endpoint_soyoustart_eu" model="cx.tower.variable.option">
<field name="variable_id" ref="variable_ovh_endpoint" />
<field name="name">soyoustart-eu</field>
<field name="reference">soyoustart-eu</field>
<field name="value_char">soyoustart-eu</field>
<field name="sequence">40</field>
</record>
<record id="option_ovh_endpoint_soyoustart_ca" model="cx.tower.variable.option">
<field name="variable_id" ref="variable_ovh_endpoint" />
<field name="name">soyoustart-ca</field>
<field name="reference">soyoustart-ca</field>
<field name="value_char">soyoustart-ca</field>
<field name="sequence">50</field>
</record>
<record id="option_ovh_endpoint_kimsufi_eu" model="cx.tower.variable.option">
<field name="variable_id" ref="variable_ovh_endpoint" />
<field name="name">kimsufi-eu</field>
<field name="reference">kimsufi-eu</field>
<field name="value_char">kimsufi-eu</field>
<field name="sequence">60</field>
</record>
<record id="option_ovh_endpoint_kimsufi_ca" model="cx.tower.variable.option">
<field name="variable_id" ref="variable_ovh_endpoint" />
<field name="name">kimsufi-ca</field>
<field name="reference">kimsufi-ca</field>
<field name="value_char">kimsufi-ca</field>
<field name="sequence">70</field>
</record>
<!-- ===============================================
Commands
================================================ -->
<!-- prettier-ignore-start -->
<record id="command_ovh_register_domain" model="cx.tower.command">
<field name="name">Register OVH DNS Record</field>
<field name="action">python_code</field>
<field name="code">
# ----------------
# Tower properties
# ----------------
cetmix_tower = env["cetmix.tower"]
# Ensure the domain in your server URL matches your OVH DNS Zone name!
# Example: If your DNS Zone is "example.com", your URL should be "*.example.com"
instance_url = {{ tower.server.url }}
ovh_endpoint = env["cx.tower.variable.option"].get_by_reference("option_ovh_endpoint_eu").value_char
ovh_application_key = #!cxtower.secret.ovh_application_key!#
ovh_application_secret = #!cxtower.secret.ovh_application_secret!#
ovh_consumer_key = #!cxtower.secret.ovh_consumer_key!#
ovh_zone_name = {{ ovh_zone_name }}
# ----------------
# Request settings
# ----------------
path = "/domain/zone/"+ovh_zone_name+"/record"
# Subdomain extraction with tldextract:
sub_domain = tldextract.extract(instance_url).subdomain
# Send request to Ovh
exit_code = 0
message = ""
try:
client = ovh.Client(
endpoint=ovh_endpoint,
application_key=ovh_application_key,
application_secret=ovh_application_secret,
consumer_key=ovh_consumer_key
)
records = client.get(path, subDomain=sub_domain, fieldType="A")
if not records:
records = client.post(
path,
fieldType="A",
subDomain=sub_domain,
target={{ tower.server.ipv4 }},
ttl=3600
)
client.post("/domain/zone/" + ovh_zone_name + "/refresh")
if records:
exit_code = 0
message = "subDomain " + sub_domain + " registered"
else:
exit_code = 0
message = "subDomain " + sub_domain + " already exists"
except Exception as e:
exit_code = 500
message = str(e)
result = {
"exit_code": exit_code,
"message": message,
}
</field>
<!-- prettier-ignore-end -->
<field name="user_ids" eval="[(6, 0, [ref('base.user_admin')])]" />
<field name="manager_ids" eval="[(6, 0, [ref('base.user_admin')])]" />
<field
name="tag_ids"
eval="[(6, 0, [ref('cetmix_tower_server.tag_custom')])]"
/>
<field
name="note"
>Register or check DNS A record for current server in OVH DNS zone</field>
</record>
</odoo>

View File

@@ -0,0 +1,30 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * cetmix_tower_ovh
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: cetmix_tower_ovh
#: model:ir.model,name:cetmix_tower_ovh.model_cx_tower_command
msgid "Cetmix Tower Command"
msgstr ""
#. module: cetmix_tower_ovh
#. odoo-python
#: code:addons/cetmix_tower_ovh/models/cx_tower_command.py:0
msgid ""
"Python 'ovh' library for OVH services. Available methods: "
"'Client'<br/>Supports OVH services<br/>Please check the <a "
"href='https://eu.api.ovh.com/console/?section=%2FallDom&branch=v1' "
"target='_blank'>OVH Documentation</a> for detailed information about the "
"services and methods."
msgstr ""

View File

@@ -0,0 +1,54 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * cetmix_tower_ovh
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: it\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.3\n"
#. module: cetmix_tower_ovh
#: model:ir.model,name:cetmix_tower_ovh.model_cx_tower_command
msgid "Cetmix Tower Command"
msgstr "Comando Cetmix Tower"
#. module: cetmix_tower_ovh
#. odoo-python
#: code:addons/cetmix_tower_ovh/models/cx_tower_command.py:0
#, python-format
msgid "Python 'ovh' library for OVH services. Available methods: 'Client'<br/>Supports OVH services<br/>Please check the <a href='https://eu.api.ovh.com/console/?section=%2FallDom&branch=v1' target='_blank'>OVH Documentation</a> for detailed information about the services and methods."
msgstr "Libreria Python 'ovh' per servizi OVH. Metodi disponibili: 'Client'<br/>Supporta i servizi OVH<br/>Verificare la <a href='https://eu.api.ovh.com/console/?section=%2FallDom&branch=v1' target='_blank'>documentazione OVH</a> per informazioni dettagliate sui servizi e i metodi."
#~ msgid "Display Name"
#~ msgstr "Nome visualizzato"
#~ msgid "ID"
#~ msgstr "ID"
#~ msgid "Last Modified on"
#~ msgstr "Ultima modifica il"
#~ msgid ""
#~ "# - ovh: Python 'ovh' library for OVH services. Available methods: "
#~ "'Client'\n"
#~ "# Supports OVH services\n"
#~ "# - tldextract: Python 'tldextract' library. Available methods: "
#~ "'extract'\n"
#~ "# Supports domain extraction"
#~ msgstr ""
#~ "# - ovh: libreria Python 'ovh' per i servizi OVH. Metodi disponibili: "
#~ "'Client'\n"
#~ "# Supporta servizi OVH\n"
#~ "# - tldextract: libreria Python 'tldextract'. Metodi disponibili: "
#~ "'extract'\n"
#~ "# Supporta estrazione dominio"

View File

@@ -0,0 +1 @@
from . import cx_tower_command # noqa: F401

View File

@@ -0,0 +1,39 @@
# Copyright 2024 Cetmix OÜ
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, models
from odoo.tools.safe_eval import wrap_module
# Wrap ovh safely
ovh = wrap_module(__import__("ovh"), ["Client"])
class CxTowerCommand(models.Model):
"""Extends cx.tower.command to add OVH functionality."""
_inherit = "cx.tower.command"
def _custom_python_libraries(self):
"""
Add the ovh library to the available libraries.
"""
python_libraries = super()._custom_python_libraries()
python_libraries.update(
{
"cetmix_tower_ovh": {
"ovh": {
"import": ovh,
"help": _(
"Python 'ovh' library for OVH services. "
"Available methods: 'Client'<br/>"
"Supports OVH services<br/>"
"Please check the <a "
"href='https://eu.api.ovh.com/console/?section=%2FallDom&branch=v1'"
" target='_blank'>OVH Documentation</a> for detailed "
"information about the services and methods."
),
}
}
}
)
return python_libraries

View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1,44 @@
# Setting up OVH Access
## Create OVH API Credentials
To use the OVH integration with Cetmix Tower, you need to create OVH API credentials:
- Follow the [official OVH documentation](https://docs.ovh.com/gb/en/api/first-steps/) (https://docs.ovh.com/gb/en/api/first-steps/) for creating an application and generating API keys
- It's recommended to create a dedicated application with appropriate permissions for Cetmix Tower
- Store your Application Key, Application Secret, and Consumer Key securely—you'll need them in the next step
## Configure OVH Secrets in Cetmix Tower
Create three secrets in Cetmix Tower to store your OVH credentials:
- Navigate to `Cetmix Tower > Settings > Keys and Secrets`
- Create a new Secret with:
- Name: `OVH Application Key`
- Reference: `ovh_application_key`
- Key Type: `Secret`
- Enter your OVH Application Key in the Secret Value tab
- Similarly, create another Secret with:
- Name: `OVH Application Secret`
- Reference: `ovh_application_secret`
- Key Type: `Secret`
- Enter your OVH Application Secret in the Secret Value tab
- Finally, create a Secret with:
- Name: `OVH Consumer Key`
- Reference: `ovh_consumer_key`
- Key Type: `Secret`
- Enter your OVH Consumer Key in the Secret Value tab
> Note: These secrets will be accessible as `#!cxtower.secret.ovh_application_key!#`, `#!cxtower.secret.ovh_application_secret!#`, and `#!cxtower.secret.ovh_consumer_key!#` in your commands.
## Configure OVH Endpoint
Create a variable to define your OVH API endpoint (region):
- Navigate to `Cetmix Tower > Settings > Variables`
- Create a new Variable with:
- Name: `OVH Endpoint`
- Reference: `ovh_endpoint`
- Type: `String`
- Set your OVH endpoint (e.g., `ovh-eu`, `ovh-ca`, `ovh-us`) as the value

View File

@@ -0,0 +1 @@
Although OVH allows API calls without using an SDK, we found that integrating the OVH SDK into Cetmix Tower makes provisioning, configuring, and maintaining OVH instances more convenient for the end user. However, not all Cetmix Tower users require this functionality, so to avoid overloading the system, we have included it in a separate module.

View File

@@ -0,0 +1 @@
This module integrates the OVH Python client, enabling seamless interaction with OVH cloud services within the Cetmix Tower.

View File

@@ -0,0 +1,86 @@
# Cetmix Tower OVHcloud Command Usage
> **Disclaimer**: The following example demonstrates one of many possible commands you can create and run with this module. The `ovh` Python library provides access to the full range of OVHcloud APIs this is just a starting point to help you get familiar with the integration.
## Example of Cetmix Tower Python Command to Create DNS Records
* **Navigate to Command Creation**
* Go to `Cetmix Tower > Commands > Commands`
* Click the `Create` button
* **Configure Command Settings**
* Set a descriptive `Name` (e.g., "List OVHcloud Instances")
* Leave `Reference` blank to generate automatically (or set a custom reference)
* Select `Action`: "Execute Python code"
* Set `Access Level`: Choose appropriate level (e.g., "Manager")
* Optional: Set `Default Path` if needed
* Optional: Add `Tags` (e.g., "ovh", "cloud", "instance") for better organization
* **Add Required Variables**
* In the `Variables` tab, add the previously configured variable:
* `ovh_endpoint` (e.g., "ovh-eu")
* **Add Required Secrets**
* In the `Secrets` field, add the previously configured secrets:
* `ovh_application_key`
* `ovh_application_secret`
* `ovh_consumer_key`
* **Write Python Code**
* Go to the `Code` tab
* Enter the following Python code:
```python
# List OVHcloud instances using ovh API
result = {"exit_code": 0, "message": None}
client = ovh.Client(
endpoint={{ ovh_endpoint }},
application_key=#!cxtower.secret.ovh_application_key!#,
application_secret=#!cxtower.secret.ovh_application_secret!#,
consumer_key=#!cxtower.secret.ovh_consumer_key!#
)
# Required variables:
# - domain_name: The main domain (e.g., "example.com")
# - subdomain: The subdomain to create (e.g., "test")
try:
# Create a new subdomain by adding a DNS entry (A record as example)
ip_address = "1.2.3.4" # Replace with the desired IP address
response = client.post(
"/domain/zone/" + domain_name + "/record",
fieldType="A",
subDomain=subdomain,
target=ip_address,
ttl=3600
)
# Refresh the zone to apply changes
client.post("/domain/zone/" + domain_name + "/refresh")
result["message"] = "Subdomain '" + subdomain + "." + domain_name + "' created and DNS zone refreshed."
except Exception as e:
result["exit_code"] = 1
result["message"] = "Error: " + str(e)
```
* **Save the Command**
* Click the `Save` button to create the command
## Running the OVHcloud Instance Command
* **Navigate to Server**
* Go to `Cetmix Tower > Servers > Servers`
* Open the server where you want to run the command
* **Execute Command from Server**
* Click the `Command` button at the top of the server form
* In the popup dialog:
* Select your OVHcloud instance command from the dropdown
* Verify the variable values (if any need adjustment)
* Click `Run` to execute
* **View Command Results**
* After execution, the command log will display showing:
* The command executed
* Execution status
* Output message containing OVHcloud instance details if successful

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,635 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Cetmix Tower OVH</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="cetmix-tower-ovh">
<h1 class="title">Cetmix Tower OVH</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:857636ba9899c2980b8ee24c3cf12b3854a7a31704c39395c7f81648d1b3ca57
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_ovh"><img alt="cetmix/cetmix-tower" src="https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github" /></a></p>
<p>This module integrates the OVH Python client, enabling seamless
interaction with OVH cloud services within the Cetmix Tower.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#use-cases-context" id="toc-entry-1">Use Cases / Context</a></li>
<li><a class="reference internal" href="#configuration" id="toc-entry-2">Configuration</a></li>
<li><a class="reference internal" href="#setting-up-ovh-access" id="toc-entry-3">Setting up OVH Access</a><ul>
<li><a class="reference internal" href="#create-ovh-api-credentials" id="toc-entry-4">Create OVH API Credentials</a></li>
<li><a class="reference internal" href="#configure-ovh-secrets-in-cetmix-tower" id="toc-entry-5">Configure OVH Secrets in Cetmix Tower</a></li>
<li><a class="reference internal" href="#configure-ovh-endpoint" id="toc-entry-6">Configure OVH Endpoint</a></li>
</ul>
</li>
<li><a class="reference internal" href="#usage" id="toc-entry-7">Usage</a></li>
<li><a class="reference internal" href="#cetmix-tower-ovhcloud-command-usage" id="toc-entry-8">Cetmix Tower OVHcloud Command Usage</a><ul>
<li><a class="reference internal" href="#example-of-cetmix-tower-python-command-to-create-dns-records" id="toc-entry-9">Example of Cetmix Tower Python Command to Create DNS Records</a></li>
<li><a class="reference internal" href="#running-the-ovhcloud-instance-command" id="toc-entry-10">Running the OVHcloud Instance Command</a></li>
</ul>
</li>
<li><a class="reference internal" href="#changelog" id="toc-entry-11">Changelog</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-12">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-13">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-14">Authors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-15">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="use-cases-context">
<h1><a class="toc-backref" href="#toc-entry-1">Use Cases / Context</a></h1>
<p>Although OVH allows API calls without using an SDK, we found that
integrating the OVH SDK into Cetmix Tower makes provisioning,
configuring, and maintaining OVH instances more convenient for the end
user. However, not all Cetmix Tower users require this functionality, so
to avoid overloading the system, we have included it in a separate
module.</p>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-2">Configuration</a></h1>
</div>
<div class="section" id="setting-up-ovh-access">
<h1><a class="toc-backref" href="#toc-entry-3">Setting up OVH Access</a></h1>
<div class="section" id="create-ovh-api-credentials">
<h2><a class="toc-backref" href="#toc-entry-4">Create OVH API Credentials</a></h2>
<p>To use the OVH integration with Cetmix Tower, you need to create OVH API
credentials:</p>
<ul class="simple">
<li>Follow the <a class="reference external" href="https://docs.ovh.com/gb/en/api/first-steps/">official OVH
documentation</a>
(<a class="reference external" href="https://docs.ovh.com/gb/en/api/first-steps/">https://docs.ovh.com/gb/en/api/first-steps/</a>) for creating an
application and generating API keys</li>
<li>Its recommended to create a dedicated application with appropriate
permissions for Cetmix Tower</li>
<li>Store your Application Key, Application Secret, and Consumer Key
securely—youll need them in the next step</li>
</ul>
</div>
<div class="section" id="configure-ovh-secrets-in-cetmix-tower">
<h2><a class="toc-backref" href="#toc-entry-5">Configure OVH Secrets in Cetmix Tower</a></h2>
<p>Create three secrets in Cetmix Tower to store your OVH credentials:</p>
<ul class="simple">
<li>Navigate to <tt class="docutils literal">Cetmix Tower &gt; Settings &gt; Keys and Secrets</tt></li>
<li>Create a new Secret with:<ul>
<li>Name: <tt class="docutils literal">OVH Application Key</tt></li>
<li>Reference: <tt class="docutils literal">ovh_application_key</tt></li>
<li>Key Type: <tt class="docutils literal">Secret</tt></li>
</ul>
</li>
<li>Enter your OVH Application Key in the Secret Value tab</li>
<li>Similarly, create another Secret with:<ul>
<li>Name: <tt class="docutils literal">OVH Application Secret</tt></li>
<li>Reference: <tt class="docutils literal">ovh_application_secret</tt></li>
<li>Key Type: <tt class="docutils literal">Secret</tt></li>
</ul>
</li>
<li>Enter your OVH Application Secret in the Secret Value tab</li>
<li>Finally, create a Secret with:<ul>
<li>Name: <tt class="docutils literal">OVH Consumer Key</tt></li>
<li>Reference: <tt class="docutils literal">ovh_consumer_key</tt></li>
<li>Key Type: <tt class="docutils literal">Secret</tt></li>
</ul>
</li>
<li>Enter your OVH Consumer Key in the Secret Value tab</li>
</ul>
<!-- -->
<blockquote>
Note: These secrets will be accessible as
<tt class="docutils literal">#!cxtower.secret.ovh_application_key!#</tt>,
<tt class="docutils literal">#!cxtower.secret.ovh_application_secret!#</tt>, and
<tt class="docutils literal">#!cxtower.secret.ovh_consumer_key!#</tt> in your commands.</blockquote>
</div>
<div class="section" id="configure-ovh-endpoint">
<h2><a class="toc-backref" href="#toc-entry-6">Configure OVH Endpoint</a></h2>
<p>Create a variable to define your OVH API endpoint (region):</p>
<ul class="simple">
<li>Navigate to <tt class="docutils literal">Cetmix Tower &gt; Settings &gt; Variables</tt></li>
<li>Create a new Variable with:<ul>
<li>Name: <tt class="docutils literal">OVH Endpoint</tt></li>
<li>Reference: <tt class="docutils literal">ovh_endpoint</tt></li>
<li>Type: <tt class="docutils literal">String</tt></li>
</ul>
</li>
<li>Set your OVH endpoint (e.g., <tt class="docutils literal"><span class="pre">ovh-eu</span></tt>, <tt class="docutils literal"><span class="pre">ovh-ca</span></tt>, <tt class="docutils literal"><span class="pre">ovh-us</span></tt>) as
the value</li>
</ul>
</div>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-7">Usage</a></h1>
</div>
<div class="section" id="cetmix-tower-ovhcloud-command-usage">
<h1><a class="toc-backref" href="#toc-entry-8">Cetmix Tower OVHcloud Command Usage</a></h1>
<blockquote>
<strong>Disclaimer</strong>: The following example demonstrates one of many
possible commands you can create and run with this module. The
<tt class="docutils literal">ovh</tt> Python library provides access to the full range of OVHcloud
APIs this is just a starting point to help you get familiar with
the integration.</blockquote>
<div class="section" id="example-of-cetmix-tower-python-command-to-create-dns-records">
<h2><a class="toc-backref" href="#toc-entry-9">Example of Cetmix Tower Python Command to Create DNS Records</a></h2>
<ul>
<li><p class="first"><strong>Navigate to Command Creation</strong></p>
<ul class="simple">
<li>Go to <tt class="docutils literal">Cetmix Tower &gt; Commands &gt; Commands</tt></li>
<li>Click the <tt class="docutils literal">Create</tt> button</li>
</ul>
</li>
<li><p class="first"><strong>Configure Command Settings</strong></p>
<ul class="simple">
<li>Set a descriptive <tt class="docutils literal">Name</tt> (e.g., “List OVHcloud Instances”)</li>
<li>Leave <tt class="docutils literal">Reference</tt> blank to generate automatically (or set a custom
reference)</li>
<li>Select <tt class="docutils literal">Action</tt>: “Execute Python code”</li>
<li>Set <tt class="docutils literal">Access Level</tt>: Choose appropriate level (e.g., “Manager”)</li>
<li>Optional: Set <tt class="docutils literal">Default Path</tt> if needed</li>
<li>Optional: Add <tt class="docutils literal">Tags</tt> (e.g., “ovh”, “cloud”, “instance”) for better
organization</li>
</ul>
</li>
<li><p class="first"><strong>Add Required Variables</strong></p>
<ul class="simple">
<li>In the <tt class="docutils literal">Variables</tt> tab, add the previously configured variable:<ul>
<li><tt class="docutils literal">ovh_endpoint</tt> (e.g., “ovh-eu”)</li>
</ul>
</li>
</ul>
</li>
<li><p class="first"><strong>Add Required Secrets</strong></p>
<ul class="simple">
<li>In the <tt class="docutils literal">Secrets</tt> field, add the previously configured secrets:<ul>
<li><tt class="docutils literal">ovh_application_key</tt></li>
<li><tt class="docutils literal">ovh_application_secret</tt></li>
<li><tt class="docutils literal">ovh_consumer_key</tt></li>
</ul>
</li>
</ul>
</li>
<li><p class="first"><strong>Write Python Code</strong></p>
<ul class="simple">
<li>Go to the <tt class="docutils literal">Code</tt> tab</li>
<li>Enter the following Python code:</li>
</ul>
<pre class="code python literal-block">
<span class="c1"># List OVHcloud instances using ovh API</span><span class="w">
</span> <span class="n">result</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&quot;exit_code&quot;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s2">&quot;message&quot;</span><span class="p">:</span> <span class="kc">None</span><span class="p">}</span><span class="w">
</span><span class="n">client</span> <span class="o">=</span> <span class="n">ovh</span><span class="o">.</span><span class="n">Client</span><span class="p">(</span><span class="w">
</span> <span class="n">endpoint</span><span class="o">=</span><span class="p">{{</span> <span class="n">ovh_endpoint</span> <span class="p">}},</span><span class="w">
</span> <span class="n">application_key</span><span class="o">=</span><span class="c1">#!cxtower.secret.ovh_application_key!#,</span><span class="w">
</span> <span class="n">application_secret</span><span class="o">=</span><span class="c1">#!cxtower.secret.ovh_application_secret!#,</span><span class="w">
</span> <span class="n">consumer_key</span><span class="o">=</span><span class="c1">#!cxtower.secret.ovh_consumer_key!#</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c1"># Required variables:</span><span class="w">
</span><span class="c1"># - domain_name: The main domain (e.g., &quot;example.com&quot;)</span><span class="w">
</span><span class="c1"># - subdomain: The subdomain to create (e.g., &quot;test&quot;)</span><span class="w">
</span><span class="k">try</span><span class="p">:</span><span class="w">
</span> <span class="c1"># Create a new subdomain by adding a DNS entry (A record as example)</span><span class="w">
</span> <span class="n">ip_address</span> <span class="o">=</span> <span class="s2">&quot;1.2.3.4&quot;</span> <span class="c1"># Replace with the desired IP address</span><span class="w">
</span> <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="w">
</span> <span class="s2">&quot;/domain/zone/&quot;</span> <span class="o">+</span> <span class="n">domain_name</span> <span class="o">+</span> <span class="s2">&quot;/record&quot;</span><span class="p">,</span><span class="w">
</span> <span class="n">fieldType</span><span class="o">=</span><span class="s2">&quot;A&quot;</span><span class="p">,</span><span class="w">
</span> <span class="n">subDomain</span><span class="o">=</span><span class="n">subdomain</span><span class="p">,</span><span class="w">
</span> <span class="n">target</span><span class="o">=</span><span class="n">ip_address</span><span class="p">,</span><span class="w">
</span> <span class="n">ttl</span><span class="o">=</span><span class="mi">3600</span><span class="w">
</span> <span class="p">)</span><span class="w">
</span> <span class="c1"># Refresh the zone to apply changes</span><span class="w">
</span> <span class="n">client</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">&quot;/domain/zone/&quot;</span> <span class="o">+</span> <span class="n">domain_name</span> <span class="o">+</span> <span class="s2">&quot;/refresh&quot;</span><span class="p">)</span><span class="w">
</span> <span class="n">result</span><span class="p">[</span><span class="s2">&quot;message&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&quot;Subdomain '&quot;</span> <span class="o">+</span> <span class="n">subdomain</span> <span class="o">+</span> <span class="s2">&quot;.&quot;</span> <span class="o">+</span> <span class="n">domain_name</span> <span class="o">+</span> <span class="s2">&quot;' created and DNS zone refreshed.&quot;</span><span class="w">
</span><span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span><span class="w">
</span> <span class="n">result</span><span class="p">[</span><span class="s2">&quot;exit_code&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span><span class="w">
</span> <span class="n">result</span><span class="p">[</span><span class="s2">&quot;message&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&quot;Error: &quot;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
</pre>
</li>
<li><p class="first"><strong>Save the Command</strong></p>
<ul class="simple">
<li>Click the <tt class="docutils literal">Save</tt> button to create the command</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="running-the-ovhcloud-instance-command">
<h2><a class="toc-backref" href="#toc-entry-10">Running the OVHcloud Instance Command</a></h2>
<ul class="simple">
<li><strong>Navigate to Server</strong><ul>
<li>Go to <tt class="docutils literal">Cetmix Tower &gt; Servers &gt; Servers</tt></li>
<li>Open the server where you want to run the command</li>
</ul>
</li>
<li><strong>Execute Command from Server</strong><ul>
<li>Click the <tt class="docutils literal">Command</tt> button at the top of the server form</li>
<li>In the popup dialog:<ul>
<li>Select your OVHcloud instance command from the dropdown</li>
<li>Verify the variable values (if any need adjustment)</li>
<li>Click <tt class="docutils literal">Run</tt> to execute</li>
</ul>
</li>
</ul>
</li>
<li><strong>View Command Results</strong><ul>
<li>After execution, the command log will display showing:<ul>
<li>The command executed</li>
<li>Execution status</li>
<li>Output message containing OVHcloud instance details if successful</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-11">Changelog</a></h1>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-12">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_ovh%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-13">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-14">Authors</a></h2>
<ul class="simple">
<li>Cetmix</li>
<li>Giovanni Serra</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-15">Maintainers</a></h2>
<p>Current maintainer:</p>
<p><a class="reference external image-reference" href="https://github.com/GSLabIt"><img alt="GSLabIt" src="https://github.com/GSLabIt.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_ovh">cetmix/cetmix-tower</a> project on GitHub.</p>
<p>You are welcome to contribute.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1 @@
from . import test_ovh_integration

View File

@@ -0,0 +1,63 @@
# Copyright 2024 Cetmix OÜ
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.tests import common
class TestOvhIntegration(common.TransactionCase):
"""Test OVH integration with Cetmix Tower commands."""
def setUp(self):
super().setUp()
# Create a test command
self.command = self.env["cx.tower.command"].create(
{
"name": "Test OVH Command",
"action": "python_code",
}
)
self.eval_context = self.env[
"cx.tower.command"
]._get_python_command_eval_context()
def test_ovh_in_evaluation_context(self):
"""Test that ovh is added to the evaluation context."""
self.assertIn("ovh", self.eval_context)
ovh_obj = self.eval_context["ovh"]
self.assertTrue(hasattr(ovh_obj, "Client"))
def test_ovh_in_evaluation_context_with_server(self):
"""Test that ovh is added to the evaluation context when server is provided."""
test_server = self.env["cx.tower.server"].create(
{
"name": "Test OVH Server",
"reference": "test_ovh_server",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"host_key": "test_key",
}
)
eval_context = self.env["cx.tower.command"]._get_python_command_eval_context(
server=test_server
)
self.assertIn("ovh", eval_context)
ovh_obj = eval_context["ovh"]
self.assertTrue(hasattr(ovh_obj, "Client"))
self.assertEqual(eval_context["server"], test_server)
def test_ovh_client_instantiation(self):
"""Test that ovh.Client can be instantiated from context."""
ovh_mod = self.eval_context["ovh"]
# Only test instantiation, do not require credentials
try:
client = ovh_mod.Client(
endpoint="ovh-eu",
application_key="a",
application_secret="b",
consumer_key="c",
)
self.assertIsNotNone(client)
except Exception as e:
self.fail(f"ovh.Client instantiation failed: {e}")

View File

@@ -0,0 +1,143 @@
===================
Cetmix Tower Server
===================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:f057dfd35f33f40155780f5855b2d2628821cd120dad4dfcf7028943a6154b47
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github
:target: https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_server
:alt: cetmix/cetmix-tower
|badge1| |badge2| |badge3|
`Cetmix Tower <https://cetmix.com/tower>`__ offers a streamlined
solution for managing remote servers and applications via SSH or API
calls directly from `Odoo <https://odoo.com>`__. It is designed for
versatility across different operating systems and software
environments, providing a practical option for those looking to manage
servers without getting tied down by vendor or technology constraints.
Please refer to the `official
documentation <https://cetmix.com/tower>`__ for detailed information.
**Table of contents**
.. contents::
:local:
Configuration
=============
Please refer to the `official
documentation <https://cetmix.com/tower>`__ for detailed configuration
instructions.
Usage
=====
Please refer to the `official
documentation <https://cetmix.com/tower>`__ for detailed usage
instructions.
Changelog
=========
18.0.2.0.0 (2026-04-07)
-----------------------
- Features: Jets! (4700)
18.0.1.0.11 (2026-03-10)
------------------------
- Bugfixes: Last flight plan line post-run action was not triggered
correctly. (5120)
18.0.1.0.10 (2026-03-10)
------------------------
- Features: Improve the 'File using template' command flow, fix the
flight plan line view layout. (5197)
18.0.1.0.9 (2026-02-19)
-----------------------
- Features: Blacklist filter for Python commands, value checker for
Vault. (5253)
18.0.1.0.7 (2026-02-05)
-----------------------
- Features: Scheduled tasks: allow to select specific days of week.
(5190)
- Bugfixes: Ensure custom values can be updated even if not provided
initially. (5175)
18.0.1.0.6 (2026-01-20)
-----------------------
- Bugfixes: Make pre-defined messages and command help translatable
again. (5174)
18.0.1.0.4 (2025-12-23)
-----------------------
- Bugfixes: Handle malformed expressions in flight plan line conditions.
(5154)
18.0.1.0.3 (2025-12-17)
-----------------------
- Features: Parse empty or missing key values as 'None' instead of
leaving key reference as is. (5134)
- Features: Improve search views, implement the search panel for
selected views. (5139)
- Bugfixes: Custom values in flight plan are lost in a skipped command
and are not available after it. (5129)
18.0.1.0.2 (2025-12-08)
-----------------------
- Bugfixes: Make variables selectable in scheduled tasks (5105)
- Bugfixes: Save correct error message in log when SSH connection fails.
(5109)
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/cetmix/cetmix-tower/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_server%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Cetmix
Maintainers
-----------
This module is part of the `cetmix/cetmix-tower <https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_server>`_ project on GitHub.
You are welcome to contribute.

Some files were not shown because too many files have changed in this diff Show More